From cd39d774a0e1e59b0c4c0726a1a1649328f2357a Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Tue, 13 Jan 2026 15:51:27 +0530 Subject: [PATCH 01/10] docs: add comprehensive V5 Calls SDK documentation - Add design specifications and requirements for V5 calling docs - Add Android Calls SDK documentation with 30+ guides covering setup, authentication, session management, audio/video controls, recording, participant management, and event listeners - Add Calls API documentation with overview, get-call, and list-calls endpoints - Add platform overview pages for Flutter, Ionic, iOS, JavaScript, and React Native - Add session settings documentation for Android SDK configuration - Update main calls.mdx page with new documentation structure - Update docs.json navigation to include new Calls SDK section and platform-specific guides - Provide complete reference for developers implementing CometChat Calls across multiple platforms --- .kiro/specs/v5-calling-docs/design.md | 212 +++++ .kiro/specs/v5-calling-docs/requirements.md | 90 +++ .kiro/specs/v5-calling-docs/tasks.md | 176 ++++ calls.mdx | 12 +- calls/android/actions.mdx | 420 ++++++++++ calls/android/audio-controls.mdx | 239 ++++++ calls/android/audio-modes.mdx | 201 +++++ calls/android/authentication.mdx | 228 ++++++ calls/android/button-click-listener.mdx | 799 +++++++++++++++++++ calls/android/call-layouts.mdx | 186 +++++ calls/android/call-logs.mdx | 295 +++++++ calls/android/events.mdx | 474 +++++++++++ calls/android/idle-timeout.mdx | 159 ++++ calls/android/join-session.mdx | 219 +++++ calls/android/layout-listener.mdx | 562 +++++++++++++ calls/android/layout-ui.mdx | 449 +++++++++++ calls/android/media-events-listener.mdx | 751 +++++++++++++++++ calls/android/overview.mdx | 97 +++ calls/android/participant-actions.mdx | 353 ++++++++ calls/android/participant-event-listener.mdx | 758 ++++++++++++++++++ calls/android/participant-management.mdx | 244 ++++++ calls/android/picture-in-picture.mdx | 204 +++++ calls/android/raise-hand.mdx | 173 ++++ calls/android/recording.mdx | 271 +++++++ calls/android/ringing.mdx | 504 ++++++++++++ calls/android/screen-sharing.mdx | 116 +++ calls/android/session-control.mdx | 437 ++++++++++ calls/android/session-status-listener.mdx | 503 ++++++++++++ calls/android/setup.mdx | 203 +++++ calls/android/video-controls.mdx | 245 ++++++ calls/api/get-call.mdx | 3 + calls/api/list-calls.mdx | 3 + calls/api/overview.mdx | 46 ++ calls/flutter/overview.mdx | 6 + calls/ionic/overview.mdx | 6 + calls/ios/overview.mdx | 6 + calls/javascript/overview.mdx | 6 + calls/react-native/overview.mdx | 6 + docs.json | 108 +++ sdk/android/calls/session-settings.mdx | 0 40 files changed, 9764 insertions(+), 6 deletions(-) create mode 100644 .kiro/specs/v5-calling-docs/design.md create mode 100644 .kiro/specs/v5-calling-docs/requirements.md create mode 100644 .kiro/specs/v5-calling-docs/tasks.md create mode 100644 calls/android/actions.mdx create mode 100644 calls/android/audio-controls.mdx create mode 100644 calls/android/audio-modes.mdx create mode 100644 calls/android/authentication.mdx create mode 100644 calls/android/button-click-listener.mdx create mode 100644 calls/android/call-layouts.mdx create mode 100644 calls/android/call-logs.mdx create mode 100644 calls/android/events.mdx create mode 100644 calls/android/idle-timeout.mdx create mode 100644 calls/android/join-session.mdx create mode 100644 calls/android/layout-listener.mdx create mode 100644 calls/android/layout-ui.mdx create mode 100644 calls/android/media-events-listener.mdx create mode 100644 calls/android/overview.mdx create mode 100644 calls/android/participant-actions.mdx create mode 100644 calls/android/participant-event-listener.mdx create mode 100644 calls/android/participant-management.mdx create mode 100644 calls/android/picture-in-picture.mdx create mode 100644 calls/android/raise-hand.mdx create mode 100644 calls/android/recording.mdx create mode 100644 calls/android/ringing.mdx create mode 100644 calls/android/screen-sharing.mdx create mode 100644 calls/android/session-control.mdx create mode 100644 calls/android/session-status-listener.mdx create mode 100644 calls/android/setup.mdx create mode 100644 calls/android/video-controls.mdx create mode 100644 calls/api/get-call.mdx create mode 100644 calls/api/list-calls.mdx create mode 100644 calls/api/overview.mdx create mode 100644 calls/flutter/overview.mdx create mode 100644 calls/ionic/overview.mdx create mode 100644 calls/ios/overview.mdx create mode 100644 calls/javascript/overview.mdx create mode 100644 calls/react-native/overview.mdx create mode 100644 sdk/android/calls/session-settings.mdx diff --git a/.kiro/specs/v5-calling-docs/design.md b/.kiro/specs/v5-calling-docs/design.md new file mode 100644 index 00000000..fa1c24d0 --- /dev/null +++ b/.kiro/specs/v5-calling-docs/design.md @@ -0,0 +1,212 @@ +# V5 Calls SDK Documentation - Design Document + +## Overview +Technical design for the V5 CometChat Calls SDK documentation structure based on SDK source code analysis. + +## SDK Architecture + +### Core Classes +1. **CometChatCalls** - Main SDK entry point (static methods) +2. **CallAppSettings** - SDK initialization configuration +3. **SessionSettings** - Call session configuration +4. **CallSession** - Active session management (singleton) +5. **Listeners** - Event handling interfaces + +### Key Methods (CometChatCalls) + +| Method | Description | +|--------|-------------| +| `init(context, callAppSettings, listener)` | Initialize SDK | +| `login(uid, apiKey, listener)` | Login with UID | +| `login(authToken, listener)` | Login with Auth Token | +| `logout(listener)` | Logout user | +| `getLoggedInUser()` | Get current user | +| `generateToken(sessionId, listener)` | Generate call token | +| `joinSession(sessionId, settings, container, listener)` | Join a session | + +### SessionSettings Options + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `title` | String | null | Session title | +| `displayName` | String | null | User display name | +| `startVideoPaused` | Boolean | false | Start with video off | +| `startAudioMuted` | Boolean | false | Start with audio muted | +| `layout` | LayoutType | TILE | TILE, SPOTLIGHT | +| `type` | SessionType | VIDEO | VIDEO, AUDIO | +| `audioMode` | AudioMode | SPEAKER | SPEAKER, EARPIECE, BLUETOOTH | +| `initialCameraFacing` | CameraFacing | FRONT | FRONT, BACK | +| `idleTimeoutPeriod` | Int | 300 | Timeout in seconds | +| `lowBandwidthMode` | Boolean | false | Enable low bandwidth | +| `autoStartRecording` | Boolean | false | Auto-start recording | +| `hideControlPanel` | Boolean | false | Hide bottom controls | +| `hideHeaderPanel` | Boolean | false | Hide top header | +| `hideLeaveSessionButton` | Boolean | false | Hide leave button | +| `hideToggleAudioButton` | Boolean | false | Hide audio toggle | +| `hideToggleVideoButton` | Boolean | false | Hide video toggle | +| `hideSwitchCameraButton` | Boolean | false | Hide camera switch | +| `hideRecordingButton` | Boolean | true | Hide recording button | +| `hideScreenSharingButton` | Boolean | false | Hide screen share | +| `hideAudioModeButton` | Boolean | false | Hide audio mode | +| `hideRaiseHandButton` | Boolean | false | Hide raise hand | +| `hideShareInviteButton` | Boolean | true | Hide share invite | +| `hideParticipantListButton` | Boolean | false | Hide participant list | +| `hideChangeLayoutButton` | Boolean | false | Hide layout change | +| `hideChatButton` | Boolean | true | Hide chat button | +| `hideSessionTimer` | Boolean | false | Hide session timer | + +### CallSession Methods + +| Category | Method | Description | +|----------|--------|-------------| +| **Audio** | `muteAudio()` | Mute local audio | +| | `unMuteAudio()` | Unmute local audio | +| | `setAudioMode(AudioMode)` | Change audio output | +| **Video** | `pauseVideo()` | Pause local video | +| | `resumeVideo()` | Resume local video | +| | `switchCamera()` | Switch front/back camera | +| **Recording** | `startRecording()` | Start session recording | +| | `stopRecording()` | Stop session recording | +| **Participants** | `muteParticipant(id)` | Mute a participant | +| | `pauseParticipantVideo(id)` | Pause participant video | +| | `pinParticipant()` | Pin participant | +| | `unPinParticipant()` | Unpin participant | +| **Hand Raise** | `raiseHand()` | Raise hand | +| | `lowerHand()` | Lower hand | +| **Layout** | `setLayout(LayoutType)` | Change call layout | +| | `enablePictureInPictureLayout()` | Enable PiP | +| | `disablePictureInPictureLayout()` | Disable PiP | +| **Session** | `leaveSession()` | Leave the session | +| | `isSessionActive()` | Check session status | +| **UI** | `setChatButtonUnreadCount(count)` | Set chat badge | + +### Listeners + +#### SessionStatusListener +- `onSessionJoined()` +- `onSessionLeft()` +- `onSessionTimedOut()` +- `onConnectionLost()` +- `onConnectionRestored()` +- `onConnectionClosed()` + +#### ParticipantEventListener +- `onParticipantJoined(Participant)` +- `onParticipantLeft(Participant)` +- `onParticipantAudioMuted(Participant)` +- `onParticipantAudioUnmuted(Participant)` +- `onParticipantVideoPaused(Participant)` +- `onParticipantVideoResumed(Participant)` +- `onParticipantHandRaised(Participant)` +- `onParticipantHandLowered(Participant)` +- `onParticipantStartedScreenShare(Participant)` +- `onParticipantStoppedScreenShare(Participant)` +- `onParticipantStartedRecording(Participant)` +- `onParticipantStoppedRecording(Participant)` +- `onDominantSpeakerChanged(Participant)` +- `onParticipantListChanged(List)` + +#### MediaEventsListener +- `onRecordingStarted()` +- `onRecordingStopped()` +- `onScreenShareStarted()` +- `onScreenShareStopped()` +- `onAudioModeChanged(AudioMode)` +- `onCameraFacingChanged(CameraFacing)` +- `onAudioMuted()` +- `onAudioUnMuted()` +- `onVideoPaused()` +- `onVideoResumed()` + +#### ButtonClickListener +- `onLeaveSessionButtonClicked()` +- `onRaiseHandButtonClicked()` +- `onShareInviteButtonClicked()` +- `onChangeLayoutButtonClicked()` +- `onParticipantListButtonClicked()` +- `onToggleAudioButtonClicked()` +- `onToggleVideoButtonClicked()` +- `onSwitchCameraButtonClicked()` +- `onChatButtonClicked()` +- `onRecordingToggleButtonClicked()` + +#### LayoutListener +- `onCallLayoutChanged(LayoutType)` +- `onParticipantListVisible()` +- `onParticipantListHidden()` +- `onPictureInPictureLayoutEnabled()` +- `onPictureInPictureLayoutDisabled()` + +## File Structure + +``` +docs/sdk/android/calls/ +├── overview.mdx +├── setup.mdx +├── authentication.mdx +├── generate-token.mdx +├── session-settings.mdx +├── join-session.mdx +├── audio-controls.mdx +├── video-controls.mdx +├── recording.mdx +├── participant-actions.mdx +├── layout-ui.mdx +├── session-control.mdx +├── session-status-listener.mdx +├── participant-event-listener.mdx +├── media-events-listener.mdx +├── button-click-listener.mdx +└── layout-listener.mdx +``` + +## Navigation Structure (docs.json) + +```json +{ + "dropdown": "Android", + "icon": "/images/icons/android.svg", + "groups": [ + { + "group": "Overview", + "pages": ["sdk/android/calls/overview"] + }, + { + "group": "Getting Started", + "pages": [ + "sdk/android/calls/setup", + "sdk/android/calls/authentication" + ] + }, + { + "group": "Join Session", + "pages": [ + "sdk/android/calls/generate-token", + "sdk/android/calls/session-settings", + "sdk/android/calls/join-session" + ] + }, + { + "group": "Session Methods", + "pages": [ + "sdk/android/calls/audio-controls", + "sdk/android/calls/video-controls", + "sdk/android/calls/recording", + "sdk/android/calls/participant-actions", + "sdk/android/calls/layout-ui", + "sdk/android/calls/session-control" + ] + }, + { + "group": "Listeners", + "pages": [ + "sdk/android/calls/session-status-listener", + "sdk/android/calls/participant-event-listener", + "sdk/android/calls/media-events-listener", + "sdk/android/calls/button-click-listener", + "sdk/android/calls/layout-listener" + ] + } + ] +} +``` diff --git a/.kiro/specs/v5-calling-docs/requirements.md b/.kiro/specs/v5-calling-docs/requirements.md new file mode 100644 index 00000000..536ef751 --- /dev/null +++ b/.kiro/specs/v5-calling-docs/requirements.md @@ -0,0 +1,90 @@ +# V5 Calls SDK Documentation + +## Overview +Create comprehensive documentation for the V5 CometChat Calls SDK covering setup, authentication, session management, methods, and listeners. + +## User Stories + +### Story 1: SDK Overview & Setup +**As a** developer new to CometChat Calls SDK +**I want** clear setup instructions and SDK overview +**So that** I can quickly integrate calling into my application + +#### Acceptance Criteria +- [ ] Overview page explains SDK capabilities and architecture +- [ ] Setup page covers installation, dependencies, and permissions +- [ ] CallAppSettings initialization is documented with examples +- [ ] Authentication methods (login with UID, authToken, logout) are documented + +### Story 2: Join Session Documentation +**As a** developer implementing calling features +**I want** detailed documentation on joining call sessions +**So that** I can properly configure and start call sessions + +#### Acceptance Criteria +- [ ] Generate Token method is documented +- [ ] SessionSettings builder with all configuration options is documented +- [ ] joinSession method with examples is documented +- [ ] CallSession object and its lifecycle is explained + +### Story 3: Session Methods Documentation +**As a** developer controlling an active call session +**I want** documentation for all CallSession methods +**So that** I can implement audio/video controls, recording, and participant management + +#### Acceptance Criteria +- [ ] Audio controls (mute, unmute, setAudioMode) are documented +- [ ] Video controls (pause, resume, switchCamera) are documented +- [ ] Recording methods (start, stop) are documented +- [ ] Participant actions (mute, pause video, pin) are documented +- [ ] Layout and UI methods are documented +- [ ] Session control methods (leave, raiseHand) are documented + +### Story 4: Listeners Documentation +**As a** developer handling call events +**I want** documentation for all listener interfaces +**So that** I can respond to session, participant, and media events + +#### Acceptance Criteria +- [ ] SessionStatusListener with all callbacks is documented +- [ ] ParticipantEventListener with all callbacks is documented +- [ ] MediaEventsListener with all callbacks is documented +- [ ] ButtonClickListener with all callbacks is documented +- [ ] LayoutListener with all callbacks is documented + +### Story 5: External Resources +**As a** developer exploring the SDK +**I want** links to sample apps and changelogs +**So that** I can see working examples and track SDK updates + +#### Acceptance Criteria +- [ ] Sample App link is included for each platform +- [ ] GitHub Changelog link is included for each platform + +## Documentation Structure (Per Platform) +``` +Calling SDK +├── Overview +├── Getting Started +│ ├── Setup +│ └── Authentication +├── Join Session +│ ├── Generate Token +│ ├── Session Settings +│ └── Join Session +├── Session Methods +│ ├── Audio Controls +│ ├── Video Controls +│ ├── Recording +│ ├── Participant Actions +│ ├── Layout & UI +│ └── Session Control +├── Listeners +│ ├── Session Status Listener +│ ├── Participant Event Listener +│ ├── Media Events Listener +│ ├── Button Click Listener +│ └── Layout Listener +├── Sample App (link) +└── Changelog (link) +``` diff --git a/.kiro/specs/v5-calling-docs/tasks.md b/.kiro/specs/v5-calling-docs/tasks.md new file mode 100644 index 00000000..fda93015 --- /dev/null +++ b/.kiro/specs/v5-calling-docs/tasks.md @@ -0,0 +1,176 @@ +# V5 Calls SDK Documentation - Implementation Tasks + +## Overview +Implementation plan for creating V5 Calls SDK documentation for all platforms, using Android as the reference implementation. + +## Tasks + +- [x] 1. Create documentation file structure + - Created `docs/calls/android/` directory with all documentation files + - _Requirements: Story 1_ + +- [x] 2. Create Overview documentation + - [x] 2.1 Create `overview.mdx` with SDK introduction, features, architecture, prerequisites + - _Requirements: Story 1_ + +- [x] 3. Create Getting Started documentation + - [x] 3.1 Create `setup.mdx` with installation, permissions, CallAppSettings initialization + - [x] 3.2 Create `authentication.mdx` with login (UID, authToken), logout, getLoggedInUser + - _Requirements: Story 1_ + +- [x] 4. Create Join Session documentation + - [x] 4.1 Create `join-session.mdx` with generateToken(), SessionSettings, joinSession() and CallSession lifecycle (consolidated) + - _Requirements: Story 2_ + +- [x] 5. Create Session Methods documentation + - [x] 5.1 Create `audio-controls.mdx` with muteAudio, unMuteAudio, setAudioMode + - [x] 5.2 Create `video-controls.mdx` with pauseVideo, resumeVideo, switchCamera + - [x] 5.3 Create `recording.mdx` with startRecording, stopRecording + - [x] 5.4 Create `participant-actions.mdx` with muteParticipant, pauseParticipantVideo, pin/unpin + - [x] 5.5 Create `layout-ui.mdx` with setLayout, PiP methods, setChatButtonUnreadCount + - [x] 5.6 Create `session-control.mdx` with leaveSession, raiseHand, lowerHand, isSessionActive + - _Requirements: Story 3_ + +- [x] 6. Create Listeners documentation + - [x] 6.1 Create `session-status-listener.mdx` with all SessionStatusListener callbacks + - [x] 6.2 Create `participant-event-listener.mdx` with all ParticipantEventListener callbacks + - [x] 6.3 Create `media-events-listener.mdx` with all MediaEventsListener callbacks + - [x] 6.4 Create `button-click-listener.mdx` with all ButtonClickListener callbacks + - [x] 6.5 Create `layout-listener.mdx` with all LayoutListener callbacks + - _Requirements: Story 4_ + +- [ ] 7. Update docs.json navigation + - [ ] 7.1 Add Calls SDK product section with Android dropdown + - [ ] 7.2 Add navigation groups: Overview, Getting Started, Join Session, Session Methods, Listeners + - [ ] 7.3 Add Sample App and Changelog external links for Android + - _Requirements: Story 5_ + +- [ ] 8. Checkpoint - Review Android documentation + - Ensure all pages are accessible and properly linked + - Verify code examples compile correctly + +- [ ] 9. Create iOS documentation + - [ ] 9.1 Create `docs/calls/ios/setup.mdx` with CocoaPods/SPM installation, permissions, initialization + - [ ] 9.2 Create `docs/calls/ios/authentication.mdx` with Swift login/logout examples + - [ ] 9.3 Create `docs/calls/ios/join-session.mdx` with generateToken, SessionSettings, joinSession + - [ ] 9.4 Create `docs/calls/ios/audio-controls.mdx` with Swift audio control examples + - [ ] 9.5 Create `docs/calls/ios/video-controls.mdx` with Swift video control examples + - [ ] 9.6 Create `docs/calls/ios/recording.mdx` with Swift recording examples + - [ ] 9.7 Create `docs/calls/ios/participant-actions.mdx` with Swift participant management + - [ ] 9.8 Create `docs/calls/ios/layout-ui.mdx` with Swift layout/UI examples + - [ ] 9.9 Create `docs/calls/ios/session-control.mdx` with Swift session control examples + - [ ] 9.10 Create listener documentation files (session-status, participant-event, media-events, button-click, layout) + - [ ] 9.11 Update `docs/calls/ios/overview.mdx` with full iOS SDK overview + - [ ] 9.12 Add iOS navigation to docs.json + - _Requirements: Stories 1-5_ + +- [ ] 10. Create React Native documentation + - [ ] 10.1 Create `docs/calls/react-native/setup.mdx` with npm/yarn installation, native setup + - [ ] 10.2 Create `docs/calls/react-native/authentication.mdx` with TypeScript login/logout examples + - [ ] 10.3 Create `docs/calls/react-native/join-session.mdx` with generateToken, SessionSettings, joinSession + - [ ] 10.4 Create `docs/calls/react-native/audio-controls.mdx` with TypeScript audio control examples + - [ ] 10.5 Create `docs/calls/react-native/video-controls.mdx` with TypeScript video control examples + - [ ] 10.6 Create `docs/calls/react-native/recording.mdx` with TypeScript recording examples + - [ ] 10.7 Create `docs/calls/react-native/participant-actions.mdx` with TypeScript participant management + - [ ] 10.8 Create `docs/calls/react-native/layout-ui.mdx` with TypeScript layout/UI examples + - [ ] 10.9 Create `docs/calls/react-native/session-control.mdx` with TypeScript session control examples + - [ ] 10.10 Create listener documentation files + - [ ] 10.11 Update `docs/calls/react-native/overview.mdx` with full React Native SDK overview + - [ ] 10.12 Add React Native navigation to docs.json + - _Requirements: Stories 1-5_ + +- [ ] 11. Create JavaScript documentation + - [ ] 11.1 Create `docs/calls/javascript/setup.mdx` with npm/CDN installation, initialization + - [ ] 11.2 Create `docs/calls/javascript/authentication.mdx` with JavaScript login/logout examples + - [ ] 11.3 Create `docs/calls/javascript/join-session.mdx` with generateToken, SessionSettings, joinSession + - [ ] 11.4 Create `docs/calls/javascript/audio-controls.mdx` with JavaScript audio control examples + - [ ] 11.5 Create `docs/calls/javascript/video-controls.mdx` with JavaScript video control examples + - [ ] 11.6 Create `docs/calls/javascript/recording.mdx` with JavaScript recording examples + - [ ] 11.7 Create `docs/calls/javascript/participant-actions.mdx` with JavaScript participant management + - [ ] 11.8 Create `docs/calls/javascript/layout-ui.mdx` with JavaScript layout/UI examples + - [ ] 11.9 Create `docs/calls/javascript/session-control.mdx` with JavaScript session control examples + - [ ] 11.10 Create listener documentation files + - [ ] 11.11 Update `docs/calls/javascript/overview.mdx` with full JavaScript SDK overview + - [ ] 11.12 Add JavaScript navigation to docs.json + - _Requirements: Stories 1-5_ + +- [ ] 12. Create Flutter documentation + - [ ] 12.1 Create `docs/calls/flutter/setup.mdx` with pub.dev installation, native setup + - [ ] 12.2 Create `docs/calls/flutter/authentication.mdx` with Dart login/logout examples + - [ ] 12.3 Create `docs/calls/flutter/join-session.mdx` with generateToken, SessionSettings, joinSession + - [ ] 12.4 Create `docs/calls/flutter/audio-controls.mdx` with Dart audio control examples + - [ ] 12.5 Create `docs/calls/flutter/video-controls.mdx` with Dart video control examples + - [ ] 12.6 Create `docs/calls/flutter/recording.mdx` with Dart recording examples + - [ ] 12.7 Create `docs/calls/flutter/participant-actions.mdx` with Dart participant management + - [ ] 12.8 Create `docs/calls/flutter/layout-ui.mdx` with Dart layout/UI examples + - [ ] 12.9 Create `docs/calls/flutter/session-control.mdx` with Dart session control examples + - [ ] 12.10 Create listener documentation files + - [ ] 12.11 Update `docs/calls/flutter/overview.mdx` with full Flutter SDK overview + - [ ] 12.12 Add Flutter navigation to docs.json + - _Requirements: Stories 1-5_ + +- [ ] 13. Create Ionic documentation + - [ ] 13.1 Create `docs/calls/ionic/setup.mdx` with npm installation, Capacitor/Cordova setup + - [ ] 13.2 Create `docs/calls/ionic/authentication.mdx` with TypeScript login/logout examples + - [ ] 13.3 Create `docs/calls/ionic/join-session.mdx` with generateToken, SessionSettings, joinSession + - [ ] 13.4 Create `docs/calls/ionic/audio-controls.mdx` with TypeScript audio control examples + - [ ] 13.5 Create `docs/calls/ionic/video-controls.mdx` with TypeScript video control examples + - [ ] 13.6 Create `docs/calls/ionic/recording.mdx` with TypeScript recording examples + - [ ] 13.7 Create `docs/calls/ionic/participant-actions.mdx` with TypeScript participant management + - [ ] 13.8 Create `docs/calls/ionic/layout-ui.mdx` with TypeScript layout/UI examples + - [ ] 13.9 Create `docs/calls/ionic/session-control.mdx` with TypeScript session control examples + - [ ] 13.10 Create listener documentation files + - [ ] 13.11 Update `docs/calls/ionic/overview.mdx` with full Ionic SDK overview + - [ ] 13.12 Add Ionic navigation to docs.json + - _Requirements: Stories 1-5_ + +- [ ] 14. Final checkpoint + - Verify all platform documentation is complete + - Test navigation across all platforms + - Ensure consistent structure and terminology + +## Current File Structure + +``` +docs/calls/ +├── android/ +│ ├── overview.mdx ✓ +│ ├── setup.mdx ✓ +│ ├── authentication.mdx ✓ +│ ├── join-session.mdx ✓ (includes generate-token & session-settings) +│ ├── audio-controls.mdx ✓ +│ ├── video-controls.mdx ✓ +│ ├── recording.mdx ✓ +│ ├── participant-actions.mdx ✓ +│ ├── layout-ui.mdx ✓ +│ ├── session-control.mdx ✓ +│ ├── session-status-listener.mdx ✓ +│ ├── participant-event-listener.mdx ✓ +│ ├── media-events-listener.mdx ✓ +│ ├── button-click-listener.mdx ✓ +│ ├── layout-listener.mdx ✓ +│ ├── listeners.mdx ✓ +│ └── session-methods.mdx ✓ +├── ios/ +│ └── overview.mdx (placeholder) +├── react-native/ +│ └── overview.mdx (placeholder) +├── javascript/ +│ └── overview.mdx (placeholder) +├── flutter/ +│ └── overview.mdx (placeholder) +├── ionic/ +│ └── overview.mdx (placeholder) +└── api/ + ├── overview.mdx ✓ + ├── get-call.mdx ✓ + └── list-calls.mdx ✓ +``` + +## Notes +- Android documentation is complete and serves as the reference implementation +- docs.json navigation needs to be updated to include the Calls SDK section +- Other platforms (iOS, React Native, JavaScript, Flutter, Ionic) need full documentation +- Use SDK source code for accurate method signatures and parameters +- Include platform-appropriate code examples (Kotlin/Java for Android, Swift for iOS, etc.) +- Ensure consistent structure across all platforms diff --git a/calls.mdx b/calls.mdx index a7d5afa1..15bf6696 100644 --- a/calls.mdx +++ b/calls.mdx @@ -79,12 +79,12 @@ import { CardGroup, Card, Icon, Badge, Steps, Columns, AccordionGroup, Accordion - } href="/sdk/javascript/calling-overview" horizontal /> - } href="/sdk/react-native/calling-overview" horizontal /> - } href="/sdk/ios/calling-overview" horizontal /> - } href="/sdk/android/calling-overview" horizontal /> - } href="/sdk/flutter/calling-overview" horizontal /> - } href="/sdk/ionic/calling-overview" horizontal /> + } href="/calls/javascript/overview" horizontal /> + } href="/calls/react-native/overview" horizontal /> + } href="/calls/ios/overview" horizontal /> + } href="/calls/android/overview" horizontal /> + } href="/calls/flutter/overview" horizontal /> + } href="/calls/ionic/overview" horizontal /> diff --git a/calls/android/actions.mdx b/calls/android/actions.mdx new file mode 100644 index 00000000..a9530747 --- /dev/null +++ b/calls/android/actions.mdx @@ -0,0 +1,420 @@ +--- +title: "Actions" +sidebarTitle: "Actions" +--- + +Use call actions to create your own custom controls or trigger call functionality dynamically based on your use case. All actions are called on the `CallSession` singleton instance during an active call session. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance + +## Get CallSession Instance + +The `CallSession` is a singleton that manages the active call. All actions are accessed through this instance. + + + +```kotlin +val callSession = CallSession.getInstance() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +``` + + + + +Always check `isSessionActive()` before calling actions to ensure there's an active call. + + +## Actions + +### Mute Audio + +Mutes your local microphone, stopping audio transmission to other participants. + + + +```kotlin +callSession.muteAudio() +``` + + +```java +callSession.muteAudio(); +``` + + + +### Unmute Audio + +Unmutes your local microphone, resuming audio transmission. + + + +```kotlin +callSession.unMuteAudio() +``` + + +```java +callSession.unMuteAudio(); +``` + + + +### Set Audio Mode + +Changes the audio output device during a call. + + + +```kotlin +callSession.setAudioMode(AudioMode.SPEAKER) +callSession.setAudioMode(AudioMode.EARPIECE) +callSession.setAudioMode(AudioMode.BLUETOOTH) +callSession.setAudioMode(AudioMode.HEADPHONES) +``` + + +```java +callSession.setAudioMode(AudioMode.SPEAKER); +callSession.setAudioMode(AudioMode.EARPIECE); +callSession.setAudioMode(AudioMode.BLUETOOTH); +callSession.setAudioMode(AudioMode.HEADPHONES); +``` + + + + +| Value | Description | +|-------|-------------| +| `SPEAKER` | Routes audio through device loudspeaker | +| `EARPIECE` | Routes audio through phone earpiece | +| `BLUETOOTH` | Routes audio through connected Bluetooth device | +| `HEADPHONES` | Routes audio through wired headphones | + + +### Pause Video + +Turns off your local camera, stopping video transmission. Other participants see your avatar. + + + +```kotlin +callSession.pauseVideo() +``` + + +```java +callSession.pauseVideo(); +``` + + + +### Resume Video + +Turns on your local camera, resuming video transmission. + + + +```kotlin +callSession.resumeVideo() +``` + + +```java +callSession.resumeVideo(); +``` + + + +### Switch Camera + +Toggles between front and back cameras without interrupting the video stream. + + + +```kotlin +callSession.switchCamera() +``` + + +```java +callSession.switchCamera(); +``` + + + +### Start Recording + +Begins server-side recording of the call. All participants are notified. + + + +```kotlin +callSession.startRecording() +``` + + +```java +callSession.startRecording(); +``` + + + + +Recording requires the feature to be enabled for your CometChat app. + + +### Stop Recording + +Stops the current recording. The recording is saved and accessible via the dashboard. + + + +```kotlin +callSession.stopRecording() +``` + + +```java +callSession.stopRecording(); +``` + + + +### Mute Participant + +Mutes a specific participant's audio. This is a moderator action. + + + +```kotlin +callSession.muteParticipant(participant.uid) +``` + + +```java +callSession.muteParticipant(participant.getUid()); +``` + + + +### Pause Participant Video + +Pauses a specific participant's video. This is a moderator action. + + + +```kotlin +callSession.pauseParticipantVideo(participant.uid) +``` + + +```java +callSession.pauseParticipantVideo(participant.getUid()); +``` + + + +### Pin Participant + +Pins a participant to keep them prominently displayed regardless of who is speaking. + + + +```kotlin +callSession.pinParticipant(participant.uid) +``` + + +```java +callSession.pinParticipant(participant.getUid()); +``` + + + +### Unpin Participant + +Removes the pin, returning to automatic speaker highlighting. + + + +```kotlin +callSession.unPinParticipant() +``` + + +```java +callSession.unPinParticipant(); +``` + + + +### Set Layout + +Changes the call layout. Each participant can choose their own layout independently. + + + +```kotlin +callSession.setLayout(LayoutType.TILE) +callSession.setLayout(LayoutType.SPOTLIGHT) +callSession.setLayout(LayoutType.SIDEBAR) +``` + + +```java +callSession.setLayout(LayoutType.TILE); +callSession.setLayout(LayoutType.SPOTLIGHT); +callSession.setLayout(LayoutType.SIDEBAR); +``` + + + + +| Value | Description | +|-------|-------------| +| `TILE` | Grid layout with equally-sized tiles | +| `SPOTLIGHT` | Large view for active speaker, small tiles for others | +| `SIDEBAR` | Main speaker with participants in a sidebar | + + +### Enable Picture In Picture Layout + +Enables PiP mode, allowing the call to continue in a floating window. + + + +```kotlin +callSession.enablePictureInPictureLayout() +``` + + +```java +callSession.enablePictureInPictureLayout(); +``` + + + +### Disable Picture In Picture Layout + +Disables PiP mode, returning to full-screen call interface. + + + +```kotlin +callSession.disablePictureInPictureLayout() +``` + + +```java +callSession.disablePictureInPictureLayout(); +``` + + + +### Set Chat Button Unread Count + +Updates the badge count on the chat button. Pass 0 to hide the badge. + + + +```kotlin +callSession.setChatButtonUnreadCount(5) +``` + + +```java +callSession.setChatButtonUnreadCount(5); +``` + + + +### Is Session Active + +Returns `true` if a call session is active, `false` otherwise. + + + +```kotlin +val isActive = callSession.isSessionActive() +``` + + +```java +boolean isActive = callSession.isSessionActive(); +``` + + + +### Leave Session + +Ends your participation and disconnects gracefully. The call continues for other participants. + + + +```kotlin +callSession.leaveSession() +``` + + +```java +callSession.leaveSession(); +``` + + + +### Raise Hand + +Shows a hand-raised indicator to get attention from other participants. + + + +```kotlin +callSession.raiseHand() +``` + + +```java +callSession.raiseHand(); +``` + + + +### Lower Hand + +Removes the hand-raised indicator. + + + +```kotlin +callSession.lowerHand() +``` + + +```java +callSession.lowerHand(); +``` + + + + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | `String` | Unique identifier (CometChat user ID) | +| `name` | `String` | Display name | +| `avatar` | `String` | URL of avatar image | +| `pid` | `String` | Participant ID for this call session | +| `role` | `String` | Role in the call | +| `audioMuted` | `Boolean` | Whether audio is muted | +| `videoPaused` | `Boolean` | Whether video is paused | +| `isPinned` | `Boolean` | Whether pinned in layout | +| `isPresenting` | `Boolean` | Whether screen sharing | +| `raisedHandTimestamp` | `Long` | Timestamp when hand was raised | + diff --git a/calls/android/audio-controls.mdx b/calls/android/audio-controls.mdx new file mode 100644 index 00000000..0a548c07 --- /dev/null +++ b/calls/android/audio-controls.mdx @@ -0,0 +1,239 @@ +--- +title: "Audio Controls" +sidebarTitle: "Audio Controls" +--- + +Control audio during an active call session. These methods allow you to mute/unmute the local microphone and change the audio output device. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance + +## Get CallSession Instance + +All audio control methods are called on the `CallSession` singleton: + + + +```kotlin +val callSession = CallSession.getInstance() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +``` + + + +--- + +## Mute Audio + +Mute the local microphone. Other participants will no longer hear you. + + + +```kotlin +callSession.muteAudio() +``` + + +```java +callSession.muteAudio(); +``` + + + + +When you mute your audio, the `onAudioMuted()` callback is triggered on your `MediaEventsListener`. + + +--- + +## Unmute Audio + +Unmute the local microphone to resume transmitting audio. + + + +```kotlin +callSession.unMuteAudio() +``` + + +```java +callSession.unMuteAudio(); +``` + + + + +When you unmute your audio, the `onAudioUnMuted()` callback is triggered on your `MediaEventsListener`. + + +--- + +## Set Audio Mode + +Change the audio output device during a call. This allows users to switch between speaker, earpiece, Bluetooth, or headphones. + + + +```kotlin +// Switch to speaker +callSession.setAudioMode(AudioMode.SPEAKER) + +// Switch to earpiece +callSession.setAudioMode(AudioMode.EARPIECE) + +// Switch to Bluetooth +callSession.setAudioMode(AudioMode.BLUETOOTH) + +// Switch to headphones +callSession.setAudioMode(AudioMode.HEADPHONES) +``` + + +```java +// Switch to speaker +callSession.setAudioMode(AudioMode.SPEAKER); + +// Switch to earpiece +callSession.setAudioMode(AudioMode.EARPIECE); + +// Switch to Bluetooth +callSession.setAudioMode(AudioMode.BLUETOOTH); + +// Switch to headphones +callSession.setAudioMode(AudioMode.HEADPHONES); +``` + + + +### AudioMode Enum + +| Value | Description | +|-------|-------------| +| `SPEAKER` | Route audio through the device speaker | +| `EARPIECE` | Route audio through the phone earpiece | +| `BLUETOOTH` | Route audio through a connected Bluetooth device | +| `HEADPHONES` | Route audio through connected wired headphones | + + +When the audio mode changes, the `onAudioModeChanged(AudioMode)` callback is triggered on your `MediaEventsListener`. + + +--- + +## Listen for Audio Events + +Register a `MediaEventsListener` to receive callbacks when audio state changes: + + + +```kotlin +callSession.addMediaEventsListener(this, object : MediaEventsListener { + override fun onAudioMuted() { + Log.d(TAG, "Audio muted") + // Update UI to show muted state + } + + override fun onAudioUnMuted() { + Log.d(TAG, "Audio unmuted") + // Update UI to show unmuted state + } + + override fun onAudioModeChanged(audioMode: AudioMode) { + Log.d(TAG, "Audio mode changed to: ${audioMode.value}") + // Update UI to reflect new audio mode + } + + // Other MediaEventsListener callbacks... + override fun onRecordingStarted() {} + override fun onRecordingStopped() {} + override fun onScreenShareStarted() {} + override fun onScreenShareStopped() {} + override fun onCameraFacingChanged(cameraFacing: CameraFacing) {} + override fun onVideoPaused() {} + override fun onVideoResumed() {} +}) +``` + + +```java +callSession.addMediaEventsListener(this, new MediaEventsListener() { + @Override + public void onAudioMuted() { + Log.d(TAG, "Audio muted"); + // Update UI to show muted state + } + + @Override + public void onAudioUnMuted() { + Log.d(TAG, "Audio unmuted"); + // Update UI to reflect unmuted state + } + + @Override + public void onAudioModeChanged(AudioMode audioMode) { + Log.d(TAG, "Audio mode changed to: " + audioMode.getValue()); + // Update UI to reflect new audio mode + } + + // Other MediaEventsListener callbacks... + @Override + public void onRecordingStarted() {} + @Override + public void onRecordingStopped() {} + @Override + public void onScreenShareStarted() {} + @Override + public void onScreenShareStopped() {} + @Override + public void onCameraFacingChanged(CameraFacing cameraFacing) {} + @Override + public void onVideoPaused() {} + @Override + public void onVideoResumed() {} +}); +``` + + + +--- + +## Initial Audio Settings + +You can configure the initial audio state when joining a session using `SessionSettings`: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .startAudioMuted(true) // Start with microphone muted + .setAudioMode(AudioMode.SPEAKER) // Start with speaker output + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .startAudioMuted(true) // Start with microphone muted + .setAudioMode(AudioMode.SPEAKER) // Start with speaker output + .build(); +``` + + + +## Next Steps + + + + Control video during calls + + + Handle all media events + + diff --git a/calls/android/audio-modes.mdx b/calls/android/audio-modes.mdx new file mode 100644 index 00000000..96186d51 --- /dev/null +++ b/calls/android/audio-modes.mdx @@ -0,0 +1,201 @@ +--- +title: "Audio Modes" +sidebarTitle: "Audio Modes" +--- + +Control audio output routing during calls. Switch between speaker, earpiece, Bluetooth, and wired headphones based on user preference or device availability. + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- User [logged in](/calls/android/authentication) +- An active [call session](/calls/android/join-session) + +## Available Audio Modes + +| Mode | Description | +|------|-------------| +| `SPEAKER` | Routes audio through the device loudspeaker | +| `EARPIECE` | Routes audio through the phone earpiece (for private calls) | +| `BLUETOOTH` | Routes audio through a connected Bluetooth device | +| `HEADPHONES` | Routes audio through wired headphones | + +## Set Initial Audio Mode + +Configure the audio mode when joining a session: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setAudioMode(AudioMode.SPEAKER) + .build() + +CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + object : CometChatCalls.CallbackListener() { + override fun onSuccess(callSession: CallSession) { + Log.d(TAG, "Joined with speaker mode") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed: ${e.message}") + } + } +) +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setAudioMode(AudioMode.SPEAKER) + .build(); + +CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession callSession) { + Log.d(TAG, "Joined with speaker mode"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed: " + e.getMessage()); + } + } +); +``` + + + +## Change Audio Mode During Call + +Switch audio modes dynamically during an active call: + + + +```kotlin +val callSession = CallSession.getInstance() + +// Switch to speaker +callSession.setAudioMode(AudioMode.SPEAKER) + +// Switch to earpiece +callSession.setAudioMode(AudioMode.EARPIECE) + +// Switch to Bluetooth +callSession.setAudioMode(AudioMode.BLUETOOTH) + +// Switch to wired headphones +callSession.setAudioMode(AudioMode.HEADPHONES) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +// Switch to speaker +callSession.setAudioMode(AudioMode.SPEAKER); + +// Switch to earpiece +callSession.setAudioMode(AudioMode.EARPIECE); + +// Switch to Bluetooth +callSession.setAudioMode(AudioMode.BLUETOOTH); + +// Switch to wired headphones +callSession.setAudioMode(AudioMode.HEADPHONES); +``` + + + +## Listen for Audio Mode Changes + +Monitor audio mode changes using `MediaEventsListener`: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addMediaEventsListener(this, object : MediaEventsListener() { + override fun onAudioModeChanged(audioMode: AudioMode) { + when (audioMode) { + AudioMode.SPEAKER -> Log.d(TAG, "Switched to speaker") + AudioMode.EARPIECE -> Log.d(TAG, "Switched to earpiece") + AudioMode.BLUETOOTH -> Log.d(TAG, "Switched to Bluetooth") + AudioMode.HEADPHONES -> Log.d(TAG, "Switched to headphones") + } + // Update audio mode button icon + updateAudioModeIcon(audioMode) + } + + // Other callbacks... + override fun onAudioMuted() {} + override fun onAudioUnMuted() {} + override fun onVideoPaused() {} + override fun onVideoResumed() {} + override fun onRecordingStarted() {} + override fun onRecordingStopped() {} + override fun onScreenShareStarted() {} + override fun onScreenShareStopped() {} + override fun onCameraFacingChanged(facing: CameraFacing) {} +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addMediaEventsListener(this, new MediaEventsListener() { + @Override + public void onAudioModeChanged(AudioMode audioMode) { + switch (audioMode) { + case SPEAKER: + Log.d(TAG, "Switched to speaker"); + break; + case EARPIECE: + Log.d(TAG, "Switched to earpiece"); + break; + case BLUETOOTH: + Log.d(TAG, "Switched to Bluetooth"); + break; + case HEADPHONES: + Log.d(TAG, "Switched to headphones"); + break; + } + // Update audio mode button icon + updateAudioModeIcon(audioMode); + } + + // Other callbacks... +}); +``` + + + +## Hide Audio Mode Button + +To prevent users from changing the audio mode, hide the button in the call UI: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setAudioMode(AudioMode.SPEAKER) // Fixed audio mode + .hideAudioModeButton(true) // Hide toggle button + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setAudioMode(AudioMode.SPEAKER) // Fixed audio mode + .hideAudioModeButton(true) // Hide toggle button + .build(); +``` + + + + +The SDK automatically detects connected audio devices. If Bluetooth or wired headphones are connected, they become available as audio mode options. + diff --git a/calls/android/authentication.mdx b/calls/android/authentication.mdx new file mode 100644 index 00000000..4e0d2738 --- /dev/null +++ b/calls/android/authentication.mdx @@ -0,0 +1,228 @@ +--- +title: "Authentication" +sidebarTitle: "Authentication" +--- + +Before users can make or receive calls, they must be authenticated with the CometChat Calls SDK. This guide covers the login and logout methods. + +## Prerequisites + +- CometChat Calls SDK must be [initialized](/calls/android/setup) +- Users must exist in your CometChat app (create via [Dashboard](https://app.cometchat.com) or [API](https://api-explorer.cometchat.com/reference/creates-user)) + + +**Sample Users** + +CometChat provides 5 test users: `cometchat-uid-1`, `cometchat-uid-2`, `cometchat-uid-3`, `cometchat-uid-4`, and `cometchat-uid-5`. + + +## Check Login Status + +Before calling `login()`, check if a user is already logged in using `getLoggedInUser()`. The SDK maintains the session internally, so you only need to login once per user session. + + + +```kotlin +val loggedInUser = CometChatCalls.getLoggedInUser() + +if (loggedInUser != null) { + // User is already logged in + Log.d(TAG, "User already logged in: ${loggedInUser.uid}") +} else { + // No user logged in, proceed with login +} +``` + + +```java +CallUser loggedInUser = CometChatCalls.getLoggedInUser(); + +if (loggedInUser != null) { + // User is already logged in + Log.d(TAG, "User already logged in: " + loggedInUser.getUid()); +} else { + // No user logged in, proceed with login +} +``` + + + +The `getLoggedInUser()` method returns a `CallUser` object if a user is logged in, or `null` if no session exists. + +## Login with UID and API Key + +This method is suitable for development and testing. For production apps, use [Auth Token login](#login-with-auth-token) instead. + + +**Security Notice** + +Using the API Key directly in client code is not recommended for production. Use Auth Token authentication for enhanced security. + + + + +```kotlin +val uid = "cometchat-uid-1" // Replace with your user's UID +val apiKey = "API_KEY" // Replace with your API Key + +if (CometChatCalls.getLoggedInUser() == null) { + CometChatCalls.login(uid, apiKey, object : CometChatCalls.CallbackListener() { + override fun onSuccess(user: CallUser) { + Log.d(TAG, "Login successful: ${user.uid}") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Login failed: ${e.message}") + } + }) +} else { + // User already logged in +} +``` + + +```java +String uid = "cometchat-uid-1"; // Replace with your user's UID +String apiKey = "API_KEY"; // Replace with your API Key + +if (CometChatCalls.getLoggedInUser() == null) { + CometChatCalls.login(uid, apiKey, new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallUser user) { + Log.d(TAG, "Login successful: " + user.getUid()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Login failed: " + e.getMessage()); + } + }); +} else { + // User already logged in +} +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `uid` | String | The unique identifier of the user to login | +| `apiKey` | String | Your CometChat API Key | +| `listener` | CallbackListener | Callback for success/error handling | + +## Login with Auth Token + +This is the recommended authentication method for production applications. The Auth Token is generated server-side, keeping your API Key secure. + +### Auth Token Flow + +1. User authenticates with your backend +2. Your backend calls the [CometChat Create Auth Token API](https://api-explorer.cometchat.com/reference/create-authtoken) +3. Your backend returns the Auth Token to the client +4. Client uses the Auth Token to login + + + +```kotlin +val authToken = "AUTH_TOKEN" // Token received from your backend + +CometChatCalls.login(authToken, object : CometChatCalls.CallbackListener() { + override fun onSuccess(user: CallUser) { + Log.d(TAG, "Login successful: ${user.uid}") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Login failed: ${e.message}") + } +}) +``` + + +```java +String authToken = "AUTH_TOKEN"; // Token received from your backend + +CometChatCalls.login(authToken, new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallUser user) { + Log.d(TAG, "Login successful: " + user.getUid()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Login failed: " + e.getMessage()); + } +}); +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `authToken` | String | Auth Token generated via CometChat API | +| `listener` | CallbackListener | Callback for success/error handling | + +## CallUser Object + +On successful login, the callback returns a `CallUser` object containing user information: + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | String | Unique identifier of the user | +| `name` | String | Display name of the user | +| `avatar` | String | URL of the user's avatar image | +| `status` | String | User's online status | + +## Logout + +Call `logout()` when the user signs out of your application. This clears the local session and disconnects from CometChat services. + + + +```kotlin +CometChatCalls.logout(object : CometChatCalls.CallbackListener() { + override fun onSuccess(message: String) { + Log.d(TAG, "Logout successful") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Logout failed: ${e.message}") + } +}) +``` + + +```java +CometChatCalls.logout(new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(String message) { + Log.d(TAG, "Logout successful"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Logout failed: " + e.getMessage()); + } +}); +``` + + + +## Error Handling + +Common authentication errors: + +| Error Code | Description | +|------------|-------------| +| `ERROR_INVALID_UID` | The provided UID is empty or invalid | +| `ERROR_UID_WITH_SPACE` | The UID contains spaces (not allowed) | +| `ERROR_API_KEY_NOT_FOUND` | The API Key is missing or invalid | +| `ERROR_BLANK_AUTHTOKEN` | The Auth Token is empty | +| `ERROR_LOGIN_IN_PROGRESS` | A login operation is already in progress | + +## Next Steps + +Once authenticated, you can start or join call sessions. + + + Generate a token and join a call session + diff --git a/calls/android/button-click-listener.mdx b/calls/android/button-click-listener.mdx new file mode 100644 index 00000000..6beadb97 --- /dev/null +++ b/calls/android/button-click-listener.mdx @@ -0,0 +1,799 @@ +--- +title: "Button Click Listener" +sidebarTitle: "Button Click Listener" +--- + +Intercept UI button clicks with `ButtonClickListener`. This listener provides callbacks when users tap buttons in the call UI, allowing you to implement custom behavior or show confirmation dialogs. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance + +## Register Listener + +Register a `ButtonClickListener` to receive button click callbacks: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addButtonClickListener(this, object : ButtonClickListener() { + override fun onLeaveSessionButtonClicked() { + Log.d(TAG, "Leave button clicked") + } + + override fun onToggleAudioButtonClicked() { + Log.d(TAG, "Audio toggle button clicked") + } + + override fun onToggleVideoButtonClicked() { + Log.d(TAG, "Video toggle button clicked") + } + + // Additional callbacks... +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onLeaveSessionButtonClicked() { + Log.d(TAG, "Leave button clicked"); + } + + @Override + public void onToggleAudioButtonClicked() { + Log.d(TAG, "Audio toggle button clicked"); + } + + @Override + public void onToggleVideoButtonClicked() { + Log.d(TAG, "Video toggle button clicked"); + } + + // Additional callbacks... +}); +``` + + + + +The listener is automatically removed when the `LifecycleOwner` (Activity/Fragment) is destroyed, preventing memory leaks. + + +--- + +## Callbacks + +### onLeaveSessionButtonClicked + +Triggered when the user taps the leave session button. + + + +```kotlin +override fun onLeaveSessionButtonClicked() { + Log.d(TAG, "Leave button clicked") + // Show confirmation dialog before leaving + showLeaveConfirmationDialog() +} + +private fun showLeaveConfirmationDialog() { + AlertDialog.Builder(this) + .setTitle("Leave Call") + .setMessage("Are you sure you want to leave this call?") + .setPositiveButton("Leave") { _, _ -> + callSession.leaveSession() + } + .setNegativeButton("Cancel", null) + .show() +} +``` + + +```java +@Override +public void onLeaveSessionButtonClicked() { + Log.d(TAG, "Leave button clicked"); + // Show confirmation dialog before leaving + showLeaveConfirmationDialog(); +} + +private void showLeaveConfirmationDialog() { + new AlertDialog.Builder(this) + .setTitle("Leave Call") + .setMessage("Are you sure you want to leave this call?") + .setPositiveButton("Leave", (dialog, which) -> { + callSession.leaveSession(); + }) + .setNegativeButton("Cancel", null) + .show(); +} +``` + + + +**Use Cases:** +- Show confirmation dialog before leaving +- Log analytics event +- Perform cleanup before leaving + +--- + +### onToggleAudioButtonClicked + +Triggered when the user taps the audio mute/unmute button. + + + +```kotlin +override fun onToggleAudioButtonClicked() { + Log.d(TAG, "Audio toggle clicked") + // Track audio toggle analytics + analytics.logEvent("audio_toggled") +} +``` + + +```java +@Override +public void onToggleAudioButtonClicked() { + Log.d(TAG, "Audio toggle clicked"); + // Track audio toggle analytics + analytics.logEvent("audio_toggled"); +} +``` + + + +**Use Cases:** +- Log analytics events +- Show tooltip on first use +- Implement custom audio toggle logic + +--- + +### onToggleVideoButtonClicked + +Triggered when the user taps the video on/off button. + + + +```kotlin +override fun onToggleVideoButtonClicked() { + Log.d(TAG, "Video toggle clicked") + // Track video toggle analytics + analytics.logEvent("video_toggled") +} +``` + + +```java +@Override +public void onToggleVideoButtonClicked() { + Log.d(TAG, "Video toggle clicked"); + // Track video toggle analytics + analytics.logEvent("video_toggled"); +} +``` + + + +**Use Cases:** +- Log analytics events +- Check camera permissions +- Implement custom video toggle logic + +--- + +### onSwitchCameraButtonClicked + +Triggered when the user taps the switch camera button. + + + +```kotlin +override fun onSwitchCameraButtonClicked() { + Log.d(TAG, "Switch camera clicked") + // Track camera switch analytics +} +``` + + +```java +@Override +public void onSwitchCameraButtonClicked() { + Log.d(TAG, "Switch camera clicked"); + // Track camera switch analytics +} +``` + + + +**Use Cases:** +- Log analytics events +- Show camera switching animation +- Track front/back camera usage + +--- + +### onRaiseHandButtonClicked + +Triggered when the user taps the raise hand button. + + + +```kotlin +override fun onRaiseHandButtonClicked() { + Log.d(TAG, "Raise hand clicked") + // Show hand raised confirmation + Toast.makeText(this, "Hand raised", Toast.LENGTH_SHORT).show() +} +``` + + +```java +@Override +public void onRaiseHandButtonClicked() { + Log.d(TAG, "Raise hand clicked"); + // Show hand raised confirmation + Toast.makeText(this, "Hand raised", Toast.LENGTH_SHORT).show(); +} +``` + + + +**Use Cases:** +- Show confirmation feedback +- Log analytics events +- Implement custom hand raise behavior + +--- + +### onShareInviteButtonClicked + +Triggered when the user taps the share invite button. + + + +```kotlin +override fun onShareInviteButtonClicked() { + Log.d(TAG, "Share invite clicked") + // Show custom share dialog + showShareDialog() +} + +private fun showShareDialog() { + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, "Join my call: https://example.com/call/$sessionId") + } + startActivity(Intent.createChooser(shareIntent, "Share call link")) +} +``` + + +```java +@Override +public void onShareInviteButtonClicked() { + Log.d(TAG, "Share invite clicked"); + // Show custom share dialog + showShareDialog(); +} + +private void showShareDialog() { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, "Join my call: https://example.com/call/" + sessionId); + startActivity(Intent.createChooser(shareIntent, "Share call link")); +} +``` + + + +**Use Cases:** +- Show custom share sheet +- Generate and share invite link +- Copy link to clipboard + +--- + +### onChangeLayoutButtonClicked + +Triggered when the user taps the change layout button. + + + +```kotlin +override fun onChangeLayoutButtonClicked() { + Log.d(TAG, "Change layout clicked") + // Show layout options dialog + showLayoutOptionsDialog() +} + +private fun showLayoutOptionsDialog() { + val layouts = arrayOf("Tile", "Spotlight") + AlertDialog.Builder(this) + .setTitle("Select Layout") + .setItems(layouts) { _, which -> + val layoutType = if (which == 0) LayoutType.TILE else LayoutType.SPOTLIGHT + callSession.setLayout(layoutType) + } + .show() +} +``` + + +```java +@Override +public void onChangeLayoutButtonClicked() { + Log.d(TAG, "Change layout clicked"); + // Show layout options dialog + showLayoutOptionsDialog(); +} + +private void showLayoutOptionsDialog() { + String[] layouts = {"Tile", "Spotlight"}; + new AlertDialog.Builder(this) + .setTitle("Select Layout") + .setItems(layouts, (dialog, which) -> { + LayoutType layoutType = (which == 0) ? LayoutType.TILE : LayoutType.SPOTLIGHT; + callSession.setLayout(layoutType); + }) + .show(); +} +``` + + + +**Use Cases:** +- Show custom layout picker +- Log layout change analytics +- Implement custom layout switching + +--- + +### onParticipantListButtonClicked + +Triggered when the user taps the participant list button. + + + +```kotlin +override fun onParticipantListButtonClicked() { + Log.d(TAG, "Participant list clicked") + // Track participant list views + analytics.logEvent("participant_list_opened") +} +``` + + +```java +@Override +public void onParticipantListButtonClicked() { + Log.d(TAG, "Participant list clicked"); + // Track participant list views + analytics.logEvent("participant_list_opened"); +} +``` + + + +**Use Cases:** +- Log analytics events +- Show custom participant list UI +- Track feature usage + +--- + +### onChatButtonClicked + +Triggered when the user taps the chat button. + + + +```kotlin +override fun onChatButtonClicked() { + Log.d(TAG, "Chat button clicked") + // Open custom chat UI + openChatScreen() +} + +private fun openChatScreen() { + // Navigate to chat screen or show chat overlay + val intent = Intent(this, ChatActivity::class.java) + intent.putExtra("sessionId", sessionId) + startActivity(intent) +} +``` + + +```java +@Override +public void onChatButtonClicked() { + Log.d(TAG, "Chat button clicked"); + // Open custom chat UI + openChatScreen(); +} + +private void openChatScreen() { + // Navigate to chat screen or show chat overlay + Intent intent = new Intent(this, ChatActivity.class); + intent.putExtra("sessionId", sessionId); + startActivity(intent); +} +``` + + + +**Use Cases:** +- Open custom chat interface +- Show in-call messaging overlay +- Navigate to chat screen + +--- + +### onRecordingToggleButtonClicked + +Triggered when the user taps the recording button. + + + +```kotlin +override fun onRecordingToggleButtonClicked() { + Log.d(TAG, "Recording toggle clicked") + // Show recording consent dialog + showRecordingConsentDialog() +} + +private fun showRecordingConsentDialog() { + AlertDialog.Builder(this) + .setTitle("Start Recording") + .setMessage("All participants will be notified that this call is being recorded.") + .setPositiveButton("Start") { _, _ -> + callSession.startRecording() + } + .setNegativeButton("Cancel", null) + .show() +} +``` + + +```java +@Override +public void onRecordingToggleButtonClicked() { + Log.d(TAG, "Recording toggle clicked"); + // Show recording consent dialog + showRecordingConsentDialog(); +} + +private void showRecordingConsentDialog() { + new AlertDialog.Builder(this) + .setTitle("Start Recording") + .setMessage("All participants will be notified that this call is being recorded.") + .setPositiveButton("Start", (dialog, which) -> { + callSession.startRecording(); + }) + .setNegativeButton("Cancel", null) + .show(); +} +``` + + + +**Use Cases:** +- Show recording consent dialog +- Check recording permissions +- Log recording analytics + +--- + +## Complete Example + +Here's a complete example handling all button click events: + + + +```kotlin +class CallActivity : AppCompatActivity() { + private lateinit var callSession: CallSession + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + callSession = CallSession.getInstance() + setupButtonClickListener() + } + + private fun setupButtonClickListener() { + callSession.addButtonClickListener(this, object : ButtonClickListener() { + override fun onLeaveSessionButtonClicked() { + showLeaveConfirmationDialog() + } + + override fun onToggleAudioButtonClicked() { + Log.d(TAG, "Audio toggle clicked") + } + + override fun onToggleVideoButtonClicked() { + Log.d(TAG, "Video toggle clicked") + } + + override fun onSwitchCameraButtonClicked() { + Log.d(TAG, "Switch camera clicked") + } + + override fun onRaiseHandButtonClicked() { + Toast.makeText( + this@CallActivity, + "Hand raised", + Toast.LENGTH_SHORT + ).show() + } + + override fun onShareInviteButtonClicked() { + shareCallLink() + } + + override fun onChangeLayoutButtonClicked() { + showLayoutOptionsDialog() + } + + override fun onParticipantListButtonClicked() { + Log.d(TAG, "Participant list opened") + } + + override fun onChatButtonClicked() { + openChatScreen() + } + + override fun onRecordingToggleButtonClicked() { + showRecordingConsentDialog() + } + }) + } + + private fun showLeaveConfirmationDialog() { + AlertDialog.Builder(this) + .setTitle("Leave Call") + .setMessage("Are you sure you want to leave?") + .setPositiveButton("Leave") { _, _ -> + callSession.leaveSession() + } + .setNegativeButton("Cancel", null) + .show() + } + + private fun shareCallLink() { + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, "Join my call!") + } + startActivity(Intent.createChooser(shareIntent, "Share")) + } + + private fun showLayoutOptionsDialog() { + val layouts = arrayOf("Tile", "Spotlight") + AlertDialog.Builder(this) + .setTitle("Select Layout") + .setItems(layouts) { _, which -> + val layoutType = if (which == 0) LayoutType.TILE else LayoutType.SPOTLIGHT + callSession.setLayout(layoutType) + } + .show() + } + + private fun openChatScreen() { + // Open chat UI + } + + private fun showRecordingConsentDialog() { + AlertDialog.Builder(this) + .setTitle("Start Recording") + .setMessage("All participants will be notified.") + .setPositiveButton("Start") { _, _ -> + callSession.startRecording() + } + .setNegativeButton("Cancel", null) + .show() + } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + private static final String TAG = "CallActivity"; + private CallSession callSession; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + callSession = CallSession.getInstance(); + setupButtonClickListener(); + } + + private void setupButtonClickListener() { + callSession.addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onLeaveSessionButtonClicked() { + showLeaveConfirmationDialog(); + } + + @Override + public void onToggleAudioButtonClicked() { + Log.d(TAG, "Audio toggle clicked"); + } + + @Override + public void onToggleVideoButtonClicked() { + Log.d(TAG, "Video toggle clicked"); + } + + @Override + public void onSwitchCameraButtonClicked() { + Log.d(TAG, "Switch camera clicked"); + } + + @Override + public void onRaiseHandButtonClicked() { + Toast.makeText( + CallActivity.this, + "Hand raised", + Toast.LENGTH_SHORT + ).show(); + } + + @Override + public void onShareInviteButtonClicked() { + shareCallLink(); + } + + @Override + public void onChangeLayoutButtonClicked() { + showLayoutOptionsDialog(); + } + + @Override + public void onParticipantListButtonClicked() { + Log.d(TAG, "Participant list opened"); + } + + @Override + public void onChatButtonClicked() { + openChatScreen(); + } + + @Override + public void onRecordingToggleButtonClicked() { + showRecordingConsentDialog(); + } + }); + } + + private void showLeaveConfirmationDialog() { + new AlertDialog.Builder(this) + .setTitle("Leave Call") + .setMessage("Are you sure you want to leave?") + .setPositiveButton("Leave", (dialog, which) -> { + callSession.leaveSession(); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + private void shareCallLink() { + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, "Join my call!"); + startActivity(Intent.createChooser(shareIntent, "Share")); + } + + private void showLayoutOptionsDialog() { + String[] layouts = {"Tile", "Spotlight"}; + new AlertDialog.Builder(this) + .setTitle("Select Layout") + .setItems(layouts, (dialog, which) -> { + LayoutType layoutType = (which == 0) ? LayoutType.TILE : LayoutType.SPOTLIGHT; + callSession.setLayout(layoutType); + }) + .show(); + } + + private void openChatScreen() { + // Open chat UI + } + + private void showRecordingConsentDialog() { + new AlertDialog.Builder(this) + .setTitle("Start Recording") + .setMessage("All participants will be notified.") + .setPositiveButton("Start", (dialog, which) -> { + callSession.startRecording(); + }) + .setNegativeButton("Cancel", null) + .show(); + } +} +``` + + + +--- + +## Callbacks Summary + +| Callback | Description | +|----------|-------------| +| `onLeaveSessionButtonClicked` | Leave session button was tapped | +| `onToggleAudioButtonClicked` | Audio mute/unmute button was tapped | +| `onToggleVideoButtonClicked` | Video on/off button was tapped | +| `onSwitchCameraButtonClicked` | Switch camera button was tapped | +| `onRaiseHandButtonClicked` | Raise hand button was tapped | +| `onShareInviteButtonClicked` | Share invite button was tapped | +| `onChangeLayoutButtonClicked` | Change layout button was tapped | +| `onParticipantListButtonClicked` | Participant list button was tapped | +| `onChatButtonClicked` | Chat button was tapped | +| `onRecordingToggleButtonClicked` | Recording toggle button was tapped | + +## Hide Buttons + +You can hide specific buttons using [SessionSettings](/calls/android/session-settings): + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideLeaveSessionButton(false) + .hideToggleAudioButton(false) + .hideToggleVideoButton(false) + .hideSwitchCameraButton(false) + .hideRaiseHandButton(false) + .hideShareInviteButton(true) + .hideChangeLayoutButton(false) + .hideParticipantListButton(false) + .hideChatButton(true) + .hideRecordingButton(true) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideLeaveSessionButton(false) + .hideToggleAudioButton(false) + .hideToggleVideoButton(false) + .hideSwitchCameraButton(false) + .hideRaiseHandButton(false) + .hideShareInviteButton(true) + .hideChangeLayoutButton(false) + .hideParticipantListButton(false) + .hideChatButton(true) + .hideRecordingButton(true) + .build(); +``` + + + +## Next Steps + + + + Handle layout change events + + + Configure button visibility + + diff --git a/calls/android/call-layouts.mdx b/calls/android/call-layouts.mdx new file mode 100644 index 00000000..4d2c8ac1 --- /dev/null +++ b/calls/android/call-layouts.mdx @@ -0,0 +1,186 @@ +--- +title: "Call Layouts" +sidebarTitle: "Call Layouts" +--- + +Choose how participants are displayed during a call. The SDK provides multiple layout options to suit different use cases like team meetings, presentations, or one-on-one calls. + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- User [logged in](/calls/android/authentication) + +## Available Layouts + +| Layout | Description | Best For | +|--------|-------------|----------| +| `TILE` | Grid layout with equally-sized tiles for all participants | Team meetings, group discussions | +| `SPOTLIGHT` | Large view for the other participant, small tile for yourself | One-on-one calls, presentations, webinars | +| `SIDEBAR` | Main speaker with participants in a sidebar | Interviews, panel discussions | + +## Set Initial Layout + +Configure the layout when joining a session using `SessionSettingsBuilder`: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setLayout(LayoutType.TILE) + .build() + +CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + object : CometChatCalls.CallbackListener() { + override fun onSuccess(callSession: CallSession) { + Log.d(TAG, "Joined with TILE layout") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed: ${e.message}") + } + } +) +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setLayout(LayoutType.TILE) + .build(); + +CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession callSession) { + Log.d(TAG, "Joined with TILE layout"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed: " + e.getMessage()); + } + } +); +``` + + + +## Change Layout During Call + +Switch layouts dynamically during an active call using `setLayout()`: + + + +```kotlin +val callSession = CallSession.getInstance() + +// Switch to Spotlight layout +callSession.setLayout(LayoutType.SPOTLIGHT) + +// Switch to Tile layout +callSession.setLayout(LayoutType.TILE) + +// Switch to Sidebar layout +callSession.setLayout(LayoutType.SIDEBAR) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +// Switch to Spotlight layout +callSession.setLayout(LayoutType.SPOTLIGHT); + +// Switch to Tile layout +callSession.setLayout(LayoutType.TILE); + +// Switch to Sidebar layout +callSession.setLayout(LayoutType.SIDEBAR); +``` + + + + +Each participant can choose their own layout independently. Changing your layout does not affect other participants. + + +## Listen for Layout Changes + +Monitor layout changes using `LayoutListener`: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addLayoutListener(this, object : LayoutListener() { + override fun onCallLayoutChanged(layoutType: LayoutType) { + when (layoutType) { + LayoutType.TILE -> Log.d(TAG, "Switched to Tile layout") + LayoutType.SPOTLIGHT -> Log.d(TAG, "Switched to Spotlight layout") + LayoutType.SIDEBAR -> Log.d(TAG, "Switched to Sidebar layout") + } + // Update layout toggle button icon + updateLayoutIcon(layoutType) + } + + override fun onParticipantListVisible() {} + override fun onParticipantListHidden() {} + override fun onPictureInPictureLayoutEnabled() {} + override fun onPictureInPictureLayoutDisabled() {} +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addLayoutListener(this, new LayoutListener() { + @Override + public void onCallLayoutChanged(LayoutType layoutType) { + switch (layoutType) { + case TILE: + Log.d(TAG, "Switched to Tile layout"); + break; + case SPOTLIGHT: + Log.d(TAG, "Switched to Spotlight layout"); + break; + case SIDEBAR: + Log.d(TAG, "Switched to Sidebar layout"); + break; + } + // Update layout toggle button icon + updateLayoutIcon(layoutType); + } + + @Override public void onParticipantListVisible() {} + @Override public void onParticipantListHidden() {} + @Override public void onPictureInPictureLayoutEnabled() {} + @Override public void onPictureInPictureLayoutDisabled() {} +}); +``` + + + +## Hide Layout Toggle Button + +To prevent users from changing the layout, hide the layout toggle button in the call UI: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setLayout(LayoutType.SPOTLIGHT) // Fixed layout + .hideChangeLayoutButton(true) // Hide toggle button + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setLayout(LayoutType.SPOTLIGHT) // Fixed layout + .hideChangeLayoutButton(true) // Hide toggle button + .build(); +``` + + diff --git a/calls/android/call-logs.mdx b/calls/android/call-logs.mdx new file mode 100644 index 00000000..96887039 --- /dev/null +++ b/calls/android/call-logs.mdx @@ -0,0 +1,295 @@ +--- +title: "Call Logs" +sidebarTitle: "Call Logs" +--- + +Retrieve call history for your application. Call logs provide detailed information about past calls including duration, participants, recordings, and status. + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- User [logged in](/calls/android/authentication) + +## Fetch Call Logs + +Use `CallLogRequest` to fetch call logs with pagination support. The builder pattern allows you to filter results by various criteria. + + + +```kotlin +val callLogRequest = CallLogRequest.CallLogRequestBuilder() + .setLimit(30) + .build() + +callLogRequest.fetchNext(object : CometChatCalls.CallbackListener>() { + override fun onSuccess(callLogs: List) { + for (callLog in callLogs) { + Log.d(TAG, "Session: ${callLog.sessionID}") + Log.d(TAG, "Duration: ${callLog.totalDuration}") + Log.d(TAG, "Status: ${callLog.status}") + } + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Error: ${e.message}") + } +}) +``` + + +```java +CallLogRequest callLogRequest = new CallLogRequest.CallLogRequestBuilder() + .setLimit(30) + .build(); + +callLogRequest.fetchNext(new CometChatCalls.CallbackListener>() { + @Override + public void onSuccess(List callLogs) { + for (CallLog callLog : callLogs) { + Log.d(TAG, "Session: " + callLog.getSessionID()); + Log.d(TAG, "Duration: " + callLog.getTotalDuration()); + Log.d(TAG, "Status: " + callLog.getStatus()); + } + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Error: " + e.getMessage()); + } +}); +``` + + + +## CallLogRequestBuilder + +Configure the request using the builder methods: + +| Method | Type | Description | +|--------|------|-------------| +| `setLimit(int)` | int | Number of call logs to fetch per request (default: 30, max: 100) | +| `setSessionType(String)` | String | Filter by call type: `video` or `audio` | +| `setCallStatus(String)` | String | Filter by call status | +| `setHasRecording(boolean)` | boolean | Filter calls that have recordings | +| `setCallCategory(String)` | String | Filter by category: `call` or `meet` | +| `setCallDirection(String)` | String | Filter by direction: `incoming` or `outgoing` | +| `setUid(String)` | String | Filter calls with a specific user | +| `setGuid(String)` | String | Filter calls with a specific group | + +### Filter Examples + + + +```kotlin +// Fetch only video calls +val videoCallsRequest = CallLogRequest.CallLogRequestBuilder() + .setSessionType("video") + .setLimit(20) + .build() + +// Fetch calls with recordings +val recordedCallsRequest = CallLogRequest.CallLogRequestBuilder() + .setHasRecording(true) + .build() + +// Fetch missed incoming calls +val missedCallsRequest = CallLogRequest.CallLogRequestBuilder() + .setCallStatus("missed") + .setCallDirection("incoming") + .build() + +// Fetch calls with a specific user +val userCallsRequest = CallLogRequest.CallLogRequestBuilder() + .setUid("user_id") + .build() +``` + + +```java +// Fetch only video calls +CallLogRequest videoCallsRequest = new CallLogRequest.CallLogRequestBuilder() + .setSessionType("video") + .setLimit(20) + .build(); + +// Fetch calls with recordings +CallLogRequest recordedCallsRequest = new CallLogRequest.CallLogRequestBuilder() + .setHasRecording(true) + .build(); + +// Fetch missed incoming calls +CallLogRequest missedCallsRequest = new CallLogRequest.CallLogRequestBuilder() + .setCallStatus("missed") + .setCallDirection("incoming") + .build(); + +// Fetch calls with a specific user +CallLogRequest userCallsRequest = new CallLogRequest.CallLogRequestBuilder() + .setUid("user_id") + .build(); +``` + + + +## Pagination + +Use `fetchNext()` and `fetchPrevious()` for pagination: + + + +```kotlin +// Fetch next page +callLogRequest.fetchNext(object : CometChatCalls.CallbackListener>() { + override fun onSuccess(callLogs: List) { + // Handle next page + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Error: ${e.message}") + } +}) + +// Fetch previous page +callLogRequest.fetchPrevious(object : CometChatCalls.CallbackListener>() { + override fun onSuccess(callLogs: List) { + // Handle previous page + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Error: ${e.message}") + } +}) +``` + + +```java +// Fetch next page +callLogRequest.fetchNext(new CometChatCalls.CallbackListener>() { + @Override + public void onSuccess(List callLogs) { + // Handle next page + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Error: " + e.getMessage()); + } +}); + +// Fetch previous page +callLogRequest.fetchPrevious(new CometChatCalls.CallbackListener>() { + @Override + public void onSuccess(List callLogs) { + // Handle previous page + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Error: " + e.getMessage()); + } +}); +``` + + + +## CallLog Object + +Each `CallLog` object contains detailed information about a call: + +| Property | Type | Description | +|----------|------|-------------| +| `sessionID` | String | Unique identifier for the call session | +| `initiator` | CallEntity | User who initiated the call | +| `receiver` | CallEntity | User or group that received the call | +| `receiverType` | String | `user` or `group` | +| `type` | String | Call type: `video` or `audio` | +| `status` | String | Final status of the call | +| `callCategory` | String | Category: `call` or `meet` | +| `initiatedAt` | long | Timestamp when call was initiated | +| `endedAt` | long | Timestamp when call ended | +| `totalDuration` | String | Human-readable duration (e.g., "5:30") | +| `totalDurationInMinutes` | double | Duration in minutes | +| `totalAudioMinutes` | double | Audio duration in minutes | +| `totalVideoMinutes` | double | Video duration in minutes | +| `totalParticipants` | int | Number of participants | +| `hasRecording` | boolean | Whether the call was recorded | +| `recordings` | List\ | List of recording objects | +| `participantInfoList` | List\ | List of participant details | + +## Access Recordings + +If a call has recordings, access them through the `recordings` property: + + + +```kotlin +callLogRequest.fetchNext(object : CometChatCalls.CallbackListener>() { + override fun onSuccess(callLogs: List) { + for (callLog in callLogs) { + if (callLog.isHasRecording) { + callLog.recordings?.forEach { recording -> + Log.d(TAG, "Recording ID: ${recording.rid}") + Log.d(TAG, "Recording URL: ${recording.recordingURL}") + Log.d(TAG, "Duration: ${recording.duration} seconds") + } + } + } + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Error: ${e.message}") + } +}) +``` + + +```java +callLogRequest.fetchNext(new CometChatCalls.CallbackListener>() { + @Override + public void onSuccess(List callLogs) { + for (CallLog callLog : callLogs) { + if (callLog.isHasRecording()) { + for (Recording recording : callLog.getRecordings()) { + Log.d(TAG, "Recording ID: " + recording.getRid()); + Log.d(TAG, "Recording URL: " + recording.getRecordingURL()); + Log.d(TAG, "Duration: " + recording.getDuration() + " seconds"); + } + } + } + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Error: " + e.getMessage()); + } +}); +``` + + + + +| Status | Description | +|--------|-------------| +| `ongoing` | Call is currently in progress | +| `busy` | Receiver was busy | +| `rejected` | Call was rejected | +| `cancelled` | Call was cancelled by initiator | +| `ended` | Call ended normally | +| `missed` | Call was missed | +| `initiated` | Call was initiated but not answered | +| `unanswered` | Call was not answered | + + + +| Category | Description | +|----------|-------------| +| `call` | Direct call between users | +| `meet` | Meeting/conference call | + + + +| Direction | Description | +|-----------|-------------| +| `incoming` | Call received by the user | +| `outgoing` | Call initiated by the user | + diff --git a/calls/android/events.mdx b/calls/android/events.mdx new file mode 100644 index 00000000..a698557f --- /dev/null +++ b/calls/android/events.mdx @@ -0,0 +1,474 @@ +--- +title: "Events" +sidebarTitle: "Events" +--- + +Handle call session events to build responsive UIs. The SDK provides five event listener interfaces to monitor session status, participant activities, media changes, button clicks, and layout changes. Each listener is lifecycle-aware and automatically cleaned up when the Activity or Fragment is destroyed. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance + +## Get CallSession Instance + +The `CallSession` is a singleton that manages the active call. All event listener registrations and session control methods are accessed through this instance. + + + +```kotlin +val callSession = CallSession.getInstance() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +``` + + + + +All event listeners are lifecycle-aware and automatically removed when the `LifecycleOwner` (Activity/Fragment) is destroyed. You don't need to manually remove listeners. + + +--- + +## Session Events + +Monitor the call session lifecycle including join/leave events and connection status. + + + +```kotlin +callSession.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionJoined() { + // Successfully connected to the session + } + + override fun onSessionLeft() { + finish() // Navigate away + } + + override fun onSessionTimedOut() { + // Session ended due to inactivity + finish() + } + + override fun onConnectionLost() { + // Network interrupted, attempting reconnection + } + + override fun onConnectionRestored() { + // Connection restored after being lost + } + + override fun onConnectionClosed() { + // Connection permanently closed + finish() + } +}) +``` + + +```java +callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionJoined() { + // Successfully connected to the session + } + + @Override + public void onSessionLeft() { + finish(); // Navigate away + } + + @Override + public void onSessionTimedOut() { + // Session ended due to inactivity + finish(); + } + + @Override + public void onConnectionLost() { + // Network interrupted, attempting reconnection + } + + @Override + public void onConnectionRestored() { + // Connection restored after being lost + } + + @Override + public void onConnectionClosed() { + // Connection permanently closed + finish(); + } +}); +``` + + + +| Event | Description | +|-------|-------------| +| `onSessionJoined()` | Successfully connected and joined the session | +| `onSessionLeft()` | Left the session via `leaveSession()` or was removed | +| `onSessionTimedOut()` | Session ended due to inactivity timeout | +| `onConnectionLost()` | Network interrupted, SDK attempting reconnection | +| `onConnectionRestored()` | Connection restored after being lost | +| `onConnectionClosed()` | Connection permanently closed, cannot reconnect | + +--- + +## Participant Events + +Monitor participant activities including join/leave, audio/video state, hand raise, screen sharing, and recording. + + + +```kotlin +callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantJoined(participant: Participant) { + // A participant joined the call + } + + override fun onParticipantLeft(participant: Participant) { + // A participant left the call + } + + override fun onParticipantListChanged(participants: List) { + // Participant list updated + } + + override fun onParticipantAudioMuted(participant: Participant) {} + override fun onParticipantAudioUnmuted(participant: Participant) {} + override fun onParticipantVideoPaused(participant: Participant) {} + override fun onParticipantVideoResumed(participant: Participant) {} + override fun onParticipantHandRaised(participant: Participant) {} + override fun onParticipantHandLowered(participant: Participant) {} + override fun onParticipantStartedScreenShare(participant: Participant) {} + override fun onParticipantStoppedScreenShare(participant: Participant) {} + override fun onParticipantStartedRecording(participant: Participant) {} + override fun onParticipantStoppedRecording(participant: Participant) {} + override fun onDominantSpeakerChanged(participant: Participant) {} +}) +``` + + +```java +callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantJoined(Participant participant) { + // A participant joined the call + } + + @Override + public void onParticipantLeft(Participant participant) { + // A participant left the call + } + + @Override + public void onParticipantListChanged(List participants) { + // Participant list updated + } + + // Other callbacks... +}); +``` + + + +| Event | Parameter | Description | +|-------|-----------|-------------| +| `onParticipantJoined` | `Participant` | A participant connected to the call | +| `onParticipantLeft` | `Participant` | A participant disconnected from the call | +| `onParticipantListChanged` | `List` | Participant list updated | +| `onParticipantAudioMuted` | `Participant` | A participant muted their microphone | +| `onParticipantAudioUnmuted` | `Participant` | A participant unmuted their microphone | +| `onParticipantVideoPaused` | `Participant` | A participant turned off their camera | +| `onParticipantVideoResumed` | `Participant` | A participant turned on their camera | +| `onParticipantHandRaised` | `Participant` | A participant raised their hand | +| `onParticipantHandLowered` | `Participant` | A participant lowered their hand | +| `onParticipantStartedScreenShare` | `Participant` | A participant started screen sharing | +| `onParticipantStoppedScreenShare` | `Participant` | A participant stopped screen sharing | +| `onParticipantStartedRecording` | `Participant` | A participant started recording | +| `onParticipantStoppedRecording` | `Participant` | A participant stopped recording | +| `onDominantSpeakerChanged` | `Participant` | The active speaker changed | + +--- + +## Media Events + +Monitor your local media state changes including audio/video status, recording, and device changes. + + + +```kotlin +callSession.addMediaEventsListener(this, object : MediaEventsListener() { + override fun onAudioMuted() { + // Your microphone was muted + } + + override fun onAudioUnMuted() { + // Your microphone was unmuted + } + + override fun onVideoPaused() { + // Your camera was turned off + } + + override fun onVideoResumed() { + // Your camera was turned on + } + + override fun onRecordingStarted() { + // Call recording started + } + + override fun onRecordingStopped() { + // Call recording stopped + } + + override fun onScreenShareStarted() {} + override fun onScreenShareStopped() {} + + override fun onAudioModeChanged(audioMode: AudioMode) { + // Audio output device changed + } + + override fun onCameraFacingChanged(facing: CameraFacing) { + // Camera switched between front and back + } +}) +``` + + +```java +callSession.addMediaEventsListener(this, new MediaEventsListener() { + @Override + public void onAudioMuted() { + // Your microphone was muted + } + + @Override + public void onAudioUnMuted() { + // Your microphone was unmuted + } + + @Override + public void onVideoPaused() { + // Your camera was turned off + } + + @Override + public void onVideoResumed() { + // Your camera was turned on + } + + @Override + public void onRecordingStarted() { + // Call recording started + } + + @Override + public void onRecordingStopped() { + // Call recording stopped + } + + @Override + public void onAudioModeChanged(AudioMode audioMode) { + // Audio output device changed + } + + @Override + public void onCameraFacingChanged(CameraFacing facing) { + // Camera switched between front and back + } + + // Other callbacks... +}); +``` + + + +| Event | Parameter | Description | +|-------|-----------|-------------| +| `onAudioMuted` | - | Your microphone was muted | +| `onAudioUnMuted` | - | Your microphone was unmuted | +| `onVideoPaused` | - | Your camera was turned off | +| `onVideoResumed` | - | Your camera was turned on | +| `onRecordingStarted` | - | Call recording started | +| `onRecordingStopped` | - | Call recording stopped | +| `onScreenShareStarted` | - | You started screen sharing | +| `onScreenShareStopped` | - | You stopped screen sharing | +| `onAudioModeChanged` | `AudioMode` | Audio output device changed | +| `onCameraFacingChanged` | `CameraFacing` | Camera switched between front and back | + + + +| Value | Description | +|-------|-------------| +| `AudioMode.SPEAKER` | Audio routed through device loudspeaker | +| `AudioMode.EARPIECE` | Audio routed through phone earpiece | +| `AudioMode.BLUETOOTH` | Audio routed through connected Bluetooth device | +| `AudioMode.HEADPHONES` | Audio routed through wired headphones | + + + +| Value | Description | +|-------|-------------| +| `CameraFacing.FRONT` | Front-facing (selfie) camera is active | +| `CameraFacing.BACK` | Rear-facing (main) camera is active | + + + +--- + +## Button Click Events + +Intercept UI button clicks from the default call interface to add custom behavior or analytics. + + + +```kotlin +callSession.addButtonClickListener(this, object : ButtonClickListener() { + override fun onLeaveSessionButtonClicked() { + // Leave button tapped + } + + override fun onToggleAudioButtonClicked() { + // Mute/unmute button tapped + } + + override fun onToggleVideoButtonClicked() { + // Video on/off button tapped + } + + override fun onSwitchCameraButtonClicked() {} + override fun onRaiseHandButtonClicked() {} + override fun onShareInviteButtonClicked() {} + override fun onChangeLayoutButtonClicked() {} + override fun onParticipantListButtonClicked() {} + override fun onChatButtonClicked() {} + override fun onRecordingToggleButtonClicked() {} +}) +``` + + +```java +callSession.addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onLeaveSessionButtonClicked() { + // Leave button tapped + } + + @Override + public void onToggleAudioButtonClicked() { + // Mute/unmute button tapped + } + + @Override + public void onToggleVideoButtonClicked() { + // Video on/off button tapped + } + + // Other callbacks... +}); +``` + + + +| Event | Description | +|-------|-------------| +| `onLeaveSessionButtonClicked` | Leave/end call button was tapped | +| `onToggleAudioButtonClicked` | Mute/unmute button was tapped | +| `onToggleVideoButtonClicked` | Video on/off button was tapped | +| `onSwitchCameraButtonClicked` | Camera switch button was tapped | +| `onRaiseHandButtonClicked` | Raise hand button was tapped | +| `onShareInviteButtonClicked` | Share/invite button was tapped | +| `onChangeLayoutButtonClicked` | Layout change button was tapped | +| `onParticipantListButtonClicked` | Participant list button was tapped | +| `onChatButtonClicked` | In-call chat button was tapped | +| `onRecordingToggleButtonClicked` | Recording toggle button was tapped | + + +Button click events fire before the SDK's default action executes. Use these to add custom logic alongside default behavior. + + +--- + +## Layout Events + +Monitor layout changes including layout type switches and Picture-in-Picture mode transitions. + + + +```kotlin +callSession.addLayoutListener(this, object : LayoutListener() { + override fun onCallLayoutChanged(layoutType: LayoutType) { + // Layout changed (TILE, SPOTLIGHT) + } + + override fun onParticipantListVisible() { + // Participant list panel opened + } + + override fun onParticipantListHidden() { + // Participant list panel closed + } + + override fun onPictureInPictureLayoutEnabled() { + // Entered PiP mode + } + + override fun onPictureInPictureLayoutDisabled() { + // Exited PiP mode + } +}) +``` + + +```java +callSession.addLayoutListener(this, new LayoutListener() { + @Override + public void onCallLayoutChanged(LayoutType layoutType) { + // Layout changed (TILE, SPOTLIGHT) + } + + @Override + public void onParticipantListVisible() { + // Participant list panel opened + } + + @Override + public void onParticipantListHidden() { + // Participant list panel closed + } + + @Override + public void onPictureInPictureLayoutEnabled() { + // Entered PiP mode + } + + @Override + public void onPictureInPictureLayoutDisabled() { + // Exited PiP mode + } +}); +``` + + + +| Event | Parameter | Description | +|-------|-----------|-------------| +| `onCallLayoutChanged` | `LayoutType` | Call layout changed | +| `onParticipantListVisible` | - | Participant list panel was opened | +| `onParticipantListHidden` | - | Participant list panel was closed | +| `onPictureInPictureLayoutEnabled` | - | Call entered Picture-in-Picture mode | +| `onPictureInPictureLayoutDisabled` | - | Call exited Picture-in-Picture mode | + + +| Value | Description | Best For | +|-------|-------------|----------| +| `LayoutType.TILE` | Grid layout with equally-sized tiles | Group discussions, team meetings | +| `LayoutType.SPOTLIGHT` | Large view for active speaker, small tiles for others | Presentations, one-on-one calls | + diff --git a/calls/android/idle-timeout.mdx b/calls/android/idle-timeout.mdx new file mode 100644 index 00000000..387f43c5 --- /dev/null +++ b/calls/android/idle-timeout.mdx @@ -0,0 +1,159 @@ +--- +title: "Idle Timeout" +sidebarTitle: "Idle Timeout" +--- + +Configure automatic session termination when a user is alone in a call. Idle timeout helps manage resources by ending sessions that have no active participants. + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- User [logged in](/calls/android/authentication) + +## How Idle Timeout Works + +When a user is the only participant in a call session, the idle timeout countdown begins. If no other participant joins before the timeout expires, the session automatically ends and the `onSessionTimedOut` callback is triggered. + +The timer also restarts when other participants leave and only one user remains in the call. + +```mermaid +flowchart LR + A[Alone in call] --> B[Timer starts] + B --> C{Participant joins?} + C -->|Yes| D[Timer stops] + C -->|No, timeout| E[Session ends] + D -->|Participant leaves| A +``` + +This is useful for: +- Preventing abandoned call sessions from running indefinitely +- Managing server resources efficiently +- Providing a better user experience when the other party doesn't join + +## Configure Idle Timeout + +Set the idle timeout period using `setIdleTimeoutPeriod()` in `SessionSettingsBuilder`. The value is in seconds. + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setIdleTimeoutPeriod(120) // 2 minutes + .setType(SessionType.VIDEO) + .build() + +CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + object : CometChatCalls.CallbackListener() { + override fun onSuccess(callSession: CallSession) { + Log.d(TAG, "Joined session") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed: ${e.message}") + } + } +) +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setIdleTimeoutPeriod(120) // 2 minutes + .setType(SessionType.VIDEO) + .build(); + +CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession callSession) { + Log.d(TAG, "Joined session"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed: " + e.getMessage()); + } + } +); +``` + + + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `idleTimeoutPeriod` | int | 300 | Timeout in seconds when alone in the session | + +## Handle Session Timeout + +Listen for the `onSessionTimedOut` callback using `SessionStatusListener` to handle when the session ends due to idle timeout: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionTimedOut() { + Log.d(TAG, "Session ended due to idle timeout") + // Show message to user + showToast("Call ended - no other participants joined") + // Navigate away from call screen + finish() + } + + override fun onSessionJoined() {} + override fun onSessionLeft() {} + override fun onConnectionLost() {} + override fun onConnectionRestored() {} + override fun onConnectionClosed() {} +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionTimedOut() { + Log.d(TAG, "Session ended due to idle timeout"); + // Show message to user + showToast("Call ended - no other participants joined"); + // Navigate away from call screen + finish(); + } + + @Override public void onSessionJoined() {} + @Override public void onSessionLeft() {} + @Override public void onConnectionLost() {} + @Override public void onConnectionRestored() {} + @Override public void onConnectionClosed() {} +}); +``` + + + +## Disable Idle Timeout + +To disable idle timeout and allow sessions to run indefinitely, set a value of `0`: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setIdleTimeoutPeriod(0) // Disable idle timeout + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setIdleTimeoutPeriod(0) // Disable idle timeout + .build(); +``` + + + + +Disabling idle timeout may result in sessions running indefinitely if participants don't join or leave properly. Use with caution. + diff --git a/calls/android/join-session.mdx b/calls/android/join-session.mdx new file mode 100644 index 00000000..28944957 --- /dev/null +++ b/calls/android/join-session.mdx @@ -0,0 +1,219 @@ +--- +title: "Join Session" +sidebarTitle: "Join Session" +--- + +Generate a token, configure settings, and join a call session. + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- User [logged in](/calls/android/authentication) +- A container view (`RelativeLayout`) for the call UI + +## Generate Token + +A call token is required for secure access to a call session. Each token is unique to a specific session and user combination, ensuring that only authorized users can join the call. + +You can generate the token just before starting the call, or generate and store it ahead of time based on your use case. + +Use the `generateToken()` method to create a call token: + + + +```kotlin +val sessionId = "SESSION_ID" + +CometChatCalls.generateToken(sessionId, object : CometChatCalls.CallbackListener() { + override fun onSuccess(token: GenerateToken) { + Log.d(TAG, "Token: ${token.token}") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Error: ${e.message}") + } +}) +``` + + +```java +String sessionId = "SESSION_ID"; + +CometChatCalls.generateToken(sessionId, new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(GenerateToken token) { + Log.d(TAG, "Token: " + token.getToken()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Error: " + e.getMessage()); + } +}); +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sessionId` | String | Unique identifier for the call session | + + +All participants joining the same call must use the same session ID. + + +## Session Settings + +Configure how a call session behaves and appears using `SessionSettingsBuilder`. + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setTitle("Team Meeting") + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .setLayout(LayoutType.TILE) + .startAudioMuted(false) + .startVideoPaused(false) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setTitle("Team Meeting") + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .setLayout(LayoutType.TILE) + .startAudioMuted(false) + .startVideoPaused(false) + .build(); +``` + + + +| Method | Type | Default | Description | +|--------|------|---------|-------------| +| `setTitle(String)` | String | null | Title displayed in the call header | +| `setDisplayName(String)` | String | null | Display name shown to other participants | +| `setType(SessionType)` | SessionType | VIDEO | Call type: `VIDEO` or `AUDIO` | +| `setLayout(LayoutType)` | LayoutType | TILE | Layout: `TILE`, `SPOTLIGHT`, or `SIDEBAR` | +| `setIdleTimeoutPeriod(int)` | int | 300 | Timeout in seconds when alone in the session | +| `startAudioMuted(boolean)` | boolean | false | Start with microphone muted | +| `startVideoPaused(boolean)` | boolean | false | Start with camera off | +| `setAudioMode(AudioMode)` | AudioMode | SPEAKER | Initial audio output device | +| `setInitialCameraFacing(CameraFacing)` | CameraFacing | FRONT | Initial camera: `FRONT` or `BACK` | +| `enableLowBandwidthMode(boolean)` | boolean | false | Optimize for poor network conditions | +| `enableAutoStartRecording(boolean)` | boolean | false | Auto-start recording when session begins | +| `hideControlPanel(boolean)` | boolean | false | Hide bottom control bar | +| `hideHeaderPanel(boolean)` | boolean | false | Hide top header bar | +| `hideSessionTimer(boolean)` | boolean | false | Hide session duration timer | +| `hideLeaveSessionButton(boolean)` | boolean | false | Hide leave/end call button | +| `hideToggleAudioButton(boolean)` | boolean | false | Hide mute/unmute button | +| `hideToggleVideoButton(boolean)` | boolean | false | Hide video on/off button | +| `hideSwitchCameraButton(boolean)` | boolean | false | Hide camera flip button | +| `hideRecordingButton(boolean)` | boolean | true | Hide recording button | +| `hideScreenSharingButton(boolean)` | boolean | false | Hide screen share button | +| `hideAudioModeButton(boolean)` | boolean | false | Hide speaker/earpiece toggle | +| `hideRaiseHandButton(boolean)` | boolean | false | Hide raise hand button | +| `hideShareInviteButton(boolean)` | boolean | true | Hide share invite button | +| `hideParticipantListButton(boolean)` | boolean | false | Hide participant list button | +| `hideChangeLayoutButton(boolean)` | boolean | false | Hide layout toggle button | +| `hideChatButton(boolean)` | boolean | true | Hide in-call chat button | + + +| Enum | Value | Description | +|------|-------|-------------| +| `SessionType` | `VIDEO` | Video call with camera enabled | +| | `AUDIO` | Audio-only call | +| `LayoutType` | `TILE` | Grid layout showing all participants equally | +| | `SPOTLIGHT` | Focus on active speaker with others in sidebar | +| | `SIDEBAR` | Main speaker with participants in a sidebar | +| `AudioMode` | `SPEAKER` | Device loudspeaker | +| | `EARPIECE` | Phone earpiece | +| | `BLUETOOTH` | Connected Bluetooth device | +| | `HEADPHONES` | Wired headphones | +| `CameraFacing` | `FRONT` | Front-facing camera | +| | `BACK` | Rear camera | + + +## Join Session + +Add a container view to your layout: + +```xml + +``` + +Use the `joinSession()` method to join a call session: + + + +```kotlin +val sessionId = "SESSION_ID" +val callViewContainer = findViewById(R.id.call_view_container) + +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .build() + +CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + object : CometChatCalls.CallbackListener() { + override fun onSuccess(callSession: CallSession) { + Log.d(TAG, "Joined session successfully") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed: ${e.message}") + } + } +) +``` + + +```java +String sessionId = "SESSION_ID"; +RelativeLayout callViewContainer = findViewById(R.id.call_view_container); + +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .build(); + +CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession callSession) { + Log.d(TAG, "Joined session successfully"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed: " + e.getMessage()); + } + } +); +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sessionId` | String | Unique session identifier (or use `GenerateToken`) | +| `sessionSettings` | SessionSettings | Configuration for the session | +| `callViewContainer` | RelativeLayout | Container view for the call UI | + +## Error Handling + +| Error Code | Description | +|------------|-------------| +| `ERROR_COMETCHAT_CALLS_SDK_INIT` | SDK not initialized | +| `ERROR_AUTH_TOKEN` | User not logged in or auth token invalid | +| `ERROR_CALL_SESSION_ID` | Session ID is null or empty | +| `ERROR_CALL_TOKEN` | Invalid or missing call token | +| `ERROR_CALLING_VIEW_REF_NULL` | Container view is null | diff --git a/calls/android/layout-listener.mdx b/calls/android/layout-listener.mdx new file mode 100644 index 00000000..b72324a6 --- /dev/null +++ b/calls/android/layout-listener.mdx @@ -0,0 +1,562 @@ +--- +title: "Layout Listener" +sidebarTitle: "Layout Listener" +--- + +Monitor layout changes with `LayoutListener`. This listener provides callbacks for call layout changes, participant list visibility, and Picture-in-Picture (PiP) mode state changes. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance + +## Register Listener + +Register a `LayoutListener` to receive layout event callbacks: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addLayoutListener(this, object : LayoutListener() { + override fun onCallLayoutChanged(layoutType: LayoutType) { + Log.d(TAG, "Layout changed to: $layoutType") + } + + override fun onParticipantListVisible() { + Log.d(TAG, "Participant list is now visible") + } + + override fun onParticipantListHidden() { + Log.d(TAG, "Participant list is now hidden") + } + + override fun onPictureInPictureLayoutEnabled() { + Log.d(TAG, "PiP mode enabled") + } + + override fun onPictureInPictureLayoutDisabled() { + Log.d(TAG, "PiP mode disabled") + } +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addLayoutListener(this, new LayoutListener() { + @Override + public void onCallLayoutChanged(LayoutType layoutType) { + Log.d(TAG, "Layout changed to: " + layoutType); + } + + @Override + public void onParticipantListVisible() { + Log.d(TAG, "Participant list is now visible"); + } + + @Override + public void onParticipantListHidden() { + Log.d(TAG, "Participant list is now hidden"); + } + + @Override + public void onPictureInPictureLayoutEnabled() { + Log.d(TAG, "PiP mode enabled"); + } + + @Override + public void onPictureInPictureLayoutDisabled() { + Log.d(TAG, "PiP mode disabled"); + } +}); +``` + + + + +The listener is automatically removed when the `LifecycleOwner` (Activity/Fragment) is destroyed, preventing memory leaks. + + +--- + +## Callbacks + +### onCallLayoutChanged + +Triggered when the call layout changes between Tile and Spotlight modes. + + + +```kotlin +override fun onCallLayoutChanged(layoutType: LayoutType) { + Log.d(TAG, "Layout changed to: $layoutType") + + when (layoutType) { + LayoutType.TILE -> { + // Update UI for tile layout + updateLayoutIcon(R.drawable.ic_grid_view) + } + LayoutType.SPOTLIGHT -> { + // Update UI for spotlight layout + updateLayoutIcon(R.drawable.ic_spotlight) + } + } +} +``` + + +```java +@Override +public void onCallLayoutChanged(LayoutType layoutType) { + Log.d(TAG, "Layout changed to: " + layoutType); + + switch (layoutType) { + case TILE: + // Update UI for tile layout + updateLayoutIcon(R.drawable.ic_grid_view); + break; + case SPOTLIGHT: + // Update UI for spotlight layout + updateLayoutIcon(R.drawable.ic_spotlight); + break; + } +} +``` + + + +**LayoutType Values:** + +| Value | Description | +|-------|-------------| +| `TILE` | Grid layout showing all participants equally | +| `SPOTLIGHT` | Focus on active speaker with others in sidebar | + +**Use Cases:** +- Update layout toggle button icon +- Adjust custom UI overlays +- Log layout preference analytics + +--- + +### onParticipantListVisible + +Triggered when the participant list panel becomes visible. + + + +```kotlin +override fun onParticipantListVisible() { + Log.d(TAG, "Participant list opened") + // Track analytics + analytics.logEvent("participant_list_opened") + // Adjust UI if needed + adjustUIForParticipantList(isVisible = true) +} +``` + + +```java +@Override +public void onParticipantListVisible() { + Log.d(TAG, "Participant list opened"); + // Track analytics + analytics.logEvent("participant_list_opened"); + // Adjust UI if needed + adjustUIForParticipantList(true); +} +``` + + + +**Use Cases:** +- Log analytics events +- Adjust custom UI elements +- Pause other UI animations + +--- + +### onParticipantListHidden + +Triggered when the participant list panel is hidden. + + + +```kotlin +override fun onParticipantListHidden() { + Log.d(TAG, "Participant list closed") + // Restore UI + adjustUIForParticipantList(isVisible = false) +} +``` + + +```java +@Override +public void onParticipantListHidden() { + Log.d(TAG, "Participant list closed"); + // Restore UI + adjustUIForParticipantList(false); +} +``` + + + +**Use Cases:** +- Restore UI elements +- Resume animations +- Update button states + +--- + +### onPictureInPictureLayoutEnabled + +Triggered when Picture-in-Picture (PiP) mode is enabled. + + + +```kotlin +override fun onPictureInPictureLayoutEnabled() { + Log.d(TAG, "PiP mode enabled") + // Hide non-essential UI elements + hideCallControls() + // Track PiP usage + analytics.logEvent("pip_enabled") +} +``` + + +```java +@Override +public void onPictureInPictureLayoutEnabled() { + Log.d(TAG, "PiP mode enabled"); + // Hide non-essential UI elements + hideCallControls(); + // Track PiP usage + analytics.logEvent("pip_enabled"); +} +``` + + + +**Use Cases:** +- Hide call control buttons +- Simplify UI for small window +- Track PiP feature usage + +--- + +### onPictureInPictureLayoutDisabled + +Triggered when Picture-in-Picture (PiP) mode is disabled. + + + +```kotlin +override fun onPictureInPictureLayoutDisabled() { + Log.d(TAG, "PiP mode disabled") + // Restore full UI + showCallControls() +} +``` + + +```java +@Override +public void onPictureInPictureLayoutDisabled() { + Log.d(TAG, "PiP mode disabled"); + // Restore full UI + showCallControls(); +} +``` + + + +**Use Cases:** +- Restore call control buttons +- Show full call UI +- Resume normal layout + +--- + +## Complete Example + +Here's a complete example handling all layout events: + + + +```kotlin +class CallActivity : AppCompatActivity() { + private lateinit var callSession: CallSession + private lateinit var layoutButton: ImageButton + private lateinit var controlsContainer: View + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + initViews() + callSession = CallSession.getInstance() + setupLayoutListener() + } + + private fun initViews() { + layoutButton = findViewById(R.id.layoutButton) + controlsContainer = findViewById(R.id.controlsContainer) + } + + private fun setupLayoutListener() { + callSession.addLayoutListener(this, object : LayoutListener() { + override fun onCallLayoutChanged(layoutType: LayoutType) { + runOnUiThread { + when (layoutType) { + LayoutType.TILE -> { + layoutButton.setImageResource(R.drawable.ic_grid_view) + layoutButton.contentDescription = "Switch to spotlight" + } + LayoutType.SPOTLIGHT -> { + layoutButton.setImageResource(R.drawable.ic_spotlight) + layoutButton.contentDescription = "Switch to tile" + } + } + } + } + + override fun onParticipantListVisible() { + runOnUiThread { + Log.d(TAG, "Participant list visible") + // Dim background or adjust layout + } + } + + override fun onParticipantListHidden() { + runOnUiThread { + Log.d(TAG, "Participant list hidden") + // Restore normal layout + } + } + + override fun onPictureInPictureLayoutEnabled() { + runOnUiThread { + Log.d(TAG, "PiP enabled") + // Hide controls for PiP mode + controlsContainer.visibility = View.GONE + } + } + + override fun onPictureInPictureLayoutDisabled() { + runOnUiThread { + Log.d(TAG, "PiP disabled") + // Show controls when exiting PiP + controlsContainer.visibility = View.VISIBLE + } + } + }) + } + + // Enable PiP when user presses home button + override fun onUserLeaveHint() { + super.onUserLeaveHint() + if (callSession.isSessionActive()) { + callSession.enablePictureInPictureLayout() + } + } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + private static final String TAG = "CallActivity"; + private CallSession callSession; + private ImageButton layoutButton; + private View controlsContainer; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + initViews(); + callSession = CallSession.getInstance(); + setupLayoutListener(); + } + + private void initViews() { + layoutButton = findViewById(R.id.layoutButton); + controlsContainer = findViewById(R.id.controlsContainer); + } + + private void setupLayoutListener() { + callSession.addLayoutListener(this, new LayoutListener() { + @Override + public void onCallLayoutChanged(LayoutType layoutType) { + runOnUiThread(() -> { + switch (layoutType) { + case TILE: + layoutButton.setImageResource(R.drawable.ic_grid_view); + layoutButton.setContentDescription("Switch to spotlight"); + break; + case SPOTLIGHT: + layoutButton.setImageResource(R.drawable.ic_spotlight); + layoutButton.setContentDescription("Switch to tile"); + break; + } + }); + } + + @Override + public void onParticipantListVisible() { + runOnUiThread(() -> { + Log.d(TAG, "Participant list visible"); + // Dim background or adjust layout + }); + } + + @Override + public void onParticipantListHidden() { + runOnUiThread(() -> { + Log.d(TAG, "Participant list hidden"); + // Restore normal layout + }); + } + + @Override + public void onPictureInPictureLayoutEnabled() { + runOnUiThread(() -> { + Log.d(TAG, "PiP enabled"); + // Hide controls for PiP mode + controlsContainer.setVisibility(View.GONE); + }); + } + + @Override + public void onPictureInPictureLayoutDisabled() { + runOnUiThread(() -> { + Log.d(TAG, "PiP disabled"); + // Show controls when exiting PiP + controlsContainer.setVisibility(View.VISIBLE); + }); + } + }); + } + + // Enable PiP when user presses home button + @Override + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + if (callSession.isSessionActive()) { + callSession.enablePictureInPictureLayout(); + } + } +} +``` + + + +--- + +## Controlling Layout Programmatically + +You can change the layout and PiP state programmatically: + +### Change Layout + + + +```kotlin +// Switch to tile layout +callSession.setLayout(LayoutType.TILE) + +// Switch to spotlight layout +callSession.setLayout(LayoutType.SPOTLIGHT) +``` + + +```java +// Switch to tile layout +callSession.setLayout(LayoutType.TILE); + +// Switch to spotlight layout +callSession.setLayout(LayoutType.SPOTLIGHT); +``` + + + +### Enable/Disable PiP + + + +```kotlin +// Enable Picture-in-Picture +callSession.enablePictureInPictureLayout() + +// Disable Picture-in-Picture +callSession.disablePictureInPictureLayout() +``` + + +```java +// Enable Picture-in-Picture +callSession.enablePictureInPictureLayout(); + +// Disable Picture-in-Picture +callSession.disablePictureInPictureLayout(); +``` + + + +--- + +## Initial Layout Configuration + +Set the initial layout when joining a session: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setLayout(LayoutType.TILE) // or LayoutType.SPOTLIGHT + .hideChangeLayoutButton(false) // Allow users to change layout + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setLayout(LayoutType.TILE) // or LayoutType.SPOTLIGHT + .hideChangeLayoutButton(false) // Allow users to change layout + .build(); +``` + + + +--- + +## Callbacks Summary + +| Callback | Parameter | Description | +|----------|-----------|-------------| +| `onCallLayoutChanged` | `LayoutType` | Call layout changed (TILE/SPOTLIGHT) | +| `onParticipantListVisible` | - | Participant list panel opened | +| `onParticipantListHidden` | - | Participant list panel closed | +| `onPictureInPictureLayoutEnabled` | - | PiP mode was enabled | +| `onPictureInPictureLayoutDisabled` | - | PiP mode was disabled | + +## Next Steps + + + + Control layout programmatically + + + Configure initial layout settings + + diff --git a/calls/android/layout-ui.mdx b/calls/android/layout-ui.mdx new file mode 100644 index 00000000..838d98c3 --- /dev/null +++ b/calls/android/layout-ui.mdx @@ -0,0 +1,449 @@ +--- +title: "Layout & UI" +sidebarTitle: "Layout & UI" +--- + +Control the call layout and UI elements during an active session. These methods allow you to change the call layout, enable Picture-in-Picture mode, and update UI badges. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance + +## Get CallSession Instance + +Layout and UI methods are called on the `CallSession` singleton: + + + +```kotlin +val callSession = CallSession.getInstance() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +``` + + + +--- + +## Set Layout + +Change the call layout during an active session. + + + +```kotlin +// Switch to tile layout (grid view) +callSession.setLayout(LayoutType.TILE) + +// Switch to spotlight layout (active speaker focus) +callSession.setLayout(LayoutType.SPOTLIGHT) + +// Switch to sidebar layout +callSession.setLayout(LayoutType.SIDEBAR) +``` + + +```java +// Switch to tile layout (grid view) +callSession.setLayout(LayoutType.TILE); + +// Switch to spotlight layout (active speaker focus) +callSession.setLayout(LayoutType.SPOTLIGHT); + +// Switch to sidebar layout +callSession.setLayout(LayoutType.SIDEBAR); +``` + + + +### LayoutType Enum + +| Value | Description | +|-------|-------------| +| `TILE` | Grid layout showing all participants equally sized | +| `SPOTLIGHT` | Focus on the active speaker with others in smaller tiles | +| `SIDEBAR` | Main speaker with participants in a sidebar | + + +When the layout changes, the `onCallLayoutChanged(LayoutType)` callback is triggered on your `LayoutListener`. + + +--- + +## Picture-in-Picture Mode + +Enable Picture-in-Picture (PiP) mode to allow users to continue viewing the call while using other apps. + +### Enable PiP + + + +```kotlin +callSession.enablePictureInPictureLayout() +``` + + +```java +callSession.enablePictureInPictureLayout(); +``` + + + +### Disable PiP + + + +```kotlin +callSession.disablePictureInPictureLayout() +``` + + +```java +callSession.disablePictureInPictureLayout(); +``` + + + +### Android PiP Integration + +To fully support PiP on Android, you need to handle the activity lifecycle: + + + +```kotlin +class CallActivity : AppCompatActivity() { + private var isInPipMode = false + + override fun onPictureInPictureModeChanged( + isInPictureInPictureMode: Boolean, + newConfig: Configuration + ) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + isInPipMode = isInPictureInPictureMode + + if (isInPictureInPictureMode) { + CallSession.getInstance().enablePictureInPictureLayout() + } else { + CallSession.getInstance().disablePictureInPictureLayout() + } + } + + override fun onUserLeaveHint() { + super.onUserLeaveHint() + // Enter PiP when user presses home button + if (CallSession.getInstance().isSessionActive()) { + enterPictureInPictureMode( + PictureInPictureParams.Builder() + .setAspectRatio(Rational(16, 9)) + .build() + ) + } + } + + override fun onStop() { + super.onStop() + if (isInPipMode && !isChangingConfigurations) { + // PiP window was closed, end the call + CallSession.getInstance().leaveSession() + } + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + private boolean isInPipMode = false; + + @Override + public void onPictureInPictureModeChanged( + boolean isInPictureInPictureMode, + Configuration newConfig) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig); + isInPipMode = isInPictureInPictureMode; + + if (isInPictureInPictureMode) { + CallSession.getInstance().enablePictureInPictureLayout(); + } else { + CallSession.getInstance().disablePictureInPictureLayout(); + } + } + + @Override + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + // Enter PiP when user presses home button + if (CallSession.getInstance().isSessionActive()) { + enterPictureInPictureMode( + new PictureInPictureParams.Builder() + .setAspectRatio(new Rational(16, 9)) + .build() + ); + } + } + + @Override + protected void onStop() { + super.onStop(); + if (isInPipMode && !isChangingConfigurations()) { + // PiP window was closed, end the call + CallSession.getInstance().leaveSession(); + } + } +} +``` + + + + +Don't forget to add `android:supportsPictureInPicture="true"` to your activity in the AndroidManifest.xml. + + +--- + +## Set Chat Button Unread Count + +Update the badge count on the chat button to show unread messages. + + + +```kotlin +// Set unread count +callSession.setChatButtonUnreadCount(5) + +// Clear unread count +callSession.setChatButtonUnreadCount(0) +``` + + +```java +// Set unread count +callSession.setChatButtonUnreadCount(5); + +// Clear unread count +callSession.setChatButtonUnreadCount(0); +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `count` | int | Number of unread messages to display on the badge | + + +The chat button must be visible (`hideChatButton(false)`) for the badge to appear. + + +--- + +## Listen for Layout Events + +Register a `LayoutListener` to receive callbacks when layout changes occur: + + + +```kotlin +callSession.addLayoutListener(this, object : LayoutListener { + override fun onCallLayoutChanged(layoutType: LayoutType) { + Log.d(TAG, "Layout changed to: ${layoutType.value}") + } + + override fun onParticipantListVisible() { + Log.d(TAG, "Participant list is now visible") + } + + override fun onParticipantListHidden() { + Log.d(TAG, "Participant list is now hidden") + } + + override fun onPictureInPictureLayoutEnabled() { + Log.d(TAG, "PiP mode enabled") + } + + override fun onPictureInPictureLayoutDisabled() { + Log.d(TAG, "PiP mode disabled") + } +}) +``` + + +```java +callSession.addLayoutListener(this, new LayoutListener() { + @Override + public void onCallLayoutChanged(LayoutType layoutType) { + Log.d(TAG, "Layout changed to: " + layoutType.getValue()); + } + + @Override + public void onParticipantListVisible() { + Log.d(TAG, "Participant list is now visible"); + } + + @Override + public void onParticipantListHidden() { + Log.d(TAG, "Participant list is now hidden"); + } + + @Override + public void onPictureInPictureLayoutEnabled() { + Log.d(TAG, "PiP mode enabled"); + } + + @Override + public void onPictureInPictureLayoutDisabled() { + Log.d(TAG, "PiP mode disabled"); + } +}); +``` + + + +--- + +## Initial Layout Settings + +Configure the initial layout when joining a session: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setLayout(LayoutType.TILE) // Start with tile layout + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setLayout(LayoutType.TILE) // Start with tile layout + .build(); +``` + + + +--- + +## Hide UI Elements + +Control the visibility of various UI elements: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + // Panels + .hideControlPanel(false) // Show bottom control bar + .hideHeaderPanel(false) // Show top header bar + .hideSessionTimer(false) // Show session duration timer + + // Buttons + .hideChangeLayoutButton(false) // Show layout toggle button + .hideChatButton(false) // Show chat button + .hideParticipantListButton(false) // Show participant list button + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + // Panels + .hideControlPanel(false) // Show bottom control bar + .hideHeaderPanel(false) // Show top header bar + .hideSessionTimer(false) // Show session duration timer + + // Buttons + .hideChangeLayoutButton(false) // Show layout toggle button + .hideChatButton(false) // Show chat button + .hideParticipantListButton(false) // Show participant list button + .build(); +``` + + + +--- + +## Button Click Listeners + +Listen for UI button clicks to implement custom behavior: + + + +```kotlin +callSession.addButtonClickListener(this, object : ButtonClickListener { + override fun onChangeLayoutButtonClicked() { + Log.d(TAG, "Layout button clicked") + } + + override fun onChatButtonClicked() { + Log.d(TAG, "Chat button clicked") + // Open your chat UI + } + + override fun onParticipantListButtonClicked() { + Log.d(TAG, "Participant list button clicked") + } + + // Other callbacks... + override fun onLeaveSessionButtonClicked() {} + override fun onRaiseHandButtonClicked() {} + override fun onShareInviteButtonClicked() {} + override fun onToggleAudioButtonClicked() {} + override fun onToggleVideoButtonClicked() {} + override fun onSwitchCameraButtonClicked() {} + override fun onRecordingToggleButtonClicked() {} +}) +``` + + +```java +callSession.addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onChangeLayoutButtonClicked() { + Log.d(TAG, "Layout button clicked"); + } + + @Override + public void onChatButtonClicked() { + Log.d(TAG, "Chat button clicked"); + // Open your chat UI + } + + @Override + public void onParticipantListButtonClicked() { + Log.d(TAG, "Participant list button clicked"); + } + + // Other callbacks... + @Override + public void onLeaveSessionButtonClicked() {} + @Override + public void onRaiseHandButtonClicked() {} + @Override + public void onShareInviteButtonClicked() {} + @Override + public void onToggleAudioButtonClicked() {} + @Override + public void onToggleVideoButtonClicked() {} + @Override + public void onSwitchCameraButtonClicked() {} + @Override + public void onRecordingToggleButtonClicked() {} +}); +``` + + + +## Next Steps + + + + Leave session and hand raise controls + + + Handle all layout events + + diff --git a/calls/android/media-events-listener.mdx b/calls/android/media-events-listener.mdx new file mode 100644 index 00000000..f9de1e9d --- /dev/null +++ b/calls/android/media-events-listener.mdx @@ -0,0 +1,751 @@ +--- +title: "Media Events Listener" +sidebarTitle: "Media Events Listener" +--- + +Monitor local media state changes with `MediaEventsListener`. This listener provides callbacks for your own audio/video state changes, recording events, screen sharing, audio mode changes, and camera facing changes. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance + +## Register Listener + +Register a `MediaEventsListener` to receive media event callbacks: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addMediaEventsListener(this, object : MediaEventsListener() { + override fun onAudioMuted() { + Log.d(TAG, "Audio muted") + } + + override fun onAudioUnMuted() { + Log.d(TAG, "Audio unmuted") + } + + override fun onVideoPaused() { + Log.d(TAG, "Video paused") + } + + override fun onVideoResumed() { + Log.d(TAG, "Video resumed") + } + + // Additional callbacks... +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addMediaEventsListener(this, new MediaEventsListener() { + @Override + public void onAudioMuted() { + Log.d(TAG, "Audio muted"); + } + + @Override + public void onAudioUnMuted() { + Log.d(TAG, "Audio unmuted"); + } + + @Override + public void onVideoPaused() { + Log.d(TAG, "Video paused"); + } + + @Override + public void onVideoResumed() { + Log.d(TAG, "Video resumed"); + } + + // Additional callbacks... +}); +``` + + + + +The listener is automatically removed when the `LifecycleOwner` (Activity/Fragment) is destroyed, preventing memory leaks. + + +--- + +## Callbacks + +### onAudioMuted + +Triggered when your local audio is muted. + + + +```kotlin +override fun onAudioMuted() { + Log.d(TAG, "Audio muted") + // Update mute button UI state + updateMuteButtonState(isMuted = true) +} +``` + + +```java +@Override +public void onAudioMuted() { + Log.d(TAG, "Audio muted"); + // Update mute button UI state + updateMuteButtonState(true); +} +``` + + + +**Use Cases:** +- Update mute button icon/state +- Show muted indicator in UI +- Sync UI with actual audio state + +--- + +### onAudioUnMuted + +Triggered when your local audio is unmuted. + + + +```kotlin +override fun onAudioUnMuted() { + Log.d(TAG, "Audio unmuted") + // Update mute button UI state + updateMuteButtonState(isMuted = false) +} +``` + + +```java +@Override +public void onAudioUnMuted() { + Log.d(TAG, "Audio unmuted"); + // Update mute button UI state + updateMuteButtonState(false); +} +``` + + + +**Use Cases:** +- Update mute button icon/state +- Hide muted indicator +- Sync UI with actual audio state + +--- + +### onVideoPaused + +Triggered when your local video is paused. + + + +```kotlin +override fun onVideoPaused() { + Log.d(TAG, "Video paused") + // Update video button UI state + updateVideoButtonState(isPaused = true) + // Show avatar instead of video preview +} +``` + + +```java +@Override +public void onVideoPaused() { + Log.d(TAG, "Video paused"); + // Update video button UI state + updateVideoButtonState(true); + // Show avatar instead of video preview +} +``` + + + +**Use Cases:** +- Update video toggle button state +- Show avatar/placeholder in local preview +- Sync UI with actual video state + +--- + +### onVideoResumed + +Triggered when your local video is resumed. + + + +```kotlin +override fun onVideoResumed() { + Log.d(TAG, "Video resumed") + // Update video button UI state + updateVideoButtonState(isPaused = false) + // Show video preview +} +``` + + +```java +@Override +public void onVideoResumed() { + Log.d(TAG, "Video resumed"); + // Update video button UI state + updateVideoButtonState(false); + // Show video preview +} +``` + + + +**Use Cases:** +- Update video toggle button state +- Show local video preview +- Sync UI with actual video state + +--- + +### onRecordingStarted + +Triggered when session recording starts. + + + +```kotlin +override fun onRecordingStarted() { + Log.d(TAG, "Recording started") + // Show recording indicator + showRecordingIndicator(true) + // Notify user that recording is active +} +``` + + +```java +@Override +public void onRecordingStarted() { + Log.d(TAG, "Recording started"); + // Show recording indicator + showRecordingIndicator(true); + // Notify user that recording is active +} +``` + + + +**Use Cases:** +- Display recording indicator (red dot) +- Update recording button state +- Show notification to participants + +--- + +### onRecordingStopped + +Triggered when session recording stops. + + + +```kotlin +override fun onRecordingStopped() { + Log.d(TAG, "Recording stopped") + // Hide recording indicator + showRecordingIndicator(false) +} +``` + + +```java +@Override +public void onRecordingStopped() { + Log.d(TAG, "Recording stopped"); + // Hide recording indicator + showRecordingIndicator(false); +} +``` + + + +**Use Cases:** +- Hide recording indicator +- Update recording button state +- Show recording saved notification + +--- + +### onScreenShareStarted + +Triggered when you start sharing your screen. + + + +```kotlin +override fun onScreenShareStarted() { + Log.d(TAG, "Screen sharing started") + // Update screen share button state + updateScreenShareButtonState(isSharing = true) + // Show screen share preview +} +``` + + +```java +@Override +public void onScreenShareStarted() { + Log.d(TAG, "Screen sharing started"); + // Update screen share button state + updateScreenShareButtonState(true); + // Show screen share preview +} +``` + + + +**Use Cases:** +- Update screen share button state +- Show "You are sharing" indicator +- Minimize local video preview + +--- + +### onScreenShareStopped + +Triggered when you stop sharing your screen. + + + +```kotlin +override fun onScreenShareStopped() { + Log.d(TAG, "Screen sharing stopped") + // Update screen share button state + updateScreenShareButtonState(isSharing = false) + // Restore normal view +} +``` + + +```java +@Override +public void onScreenShareStopped() { + Log.d(TAG, "Screen sharing stopped"); + // Update screen share button state + updateScreenShareButtonState(false); + // Restore normal view +} +``` + + + +**Use Cases:** +- Update screen share button state +- Hide "You are sharing" indicator +- Restore local video preview + +--- + +### onAudioModeChanged + +Triggered when the audio output mode changes (speaker, earpiece, bluetooth). + + + +```kotlin +override fun onAudioModeChanged(audioMode: AudioMode) { + Log.d(TAG, "Audio mode changed to: $audioMode") + // Update audio mode button/icon + when (audioMode) { + AudioMode.SPEAKER -> updateAudioModeIcon(R.drawable.ic_speaker) + AudioMode.EARPIECE -> updateAudioModeIcon(R.drawable.ic_earpiece) + AudioMode.BLUETOOTH -> updateAudioModeIcon(R.drawable.ic_bluetooth) + } +} +``` + + +```java +@Override +public void onAudioModeChanged(AudioMode audioMode) { + Log.d(TAG, "Audio mode changed to: " + audioMode); + // Update audio mode button/icon + switch (audioMode) { + case SPEAKER: + updateAudioModeIcon(R.drawable.ic_speaker); + break; + case EARPIECE: + updateAudioModeIcon(R.drawable.ic_earpiece); + break; + case BLUETOOTH: + updateAudioModeIcon(R.drawable.ic_bluetooth); + break; + } +} +``` + + + +**AudioMode Values:** + +| Value | Description | +|-------|-------------| +| `SPEAKER` | Audio output through device speaker | +| `EARPIECE` | Audio output through earpiece | +| `BLUETOOTH` | Audio output through connected Bluetooth device | + +**Use Cases:** +- Update audio mode button icon +- Show current audio output device +- Handle Bluetooth connection/disconnection + +--- + +### onCameraFacingChanged + +Triggered when the camera facing changes (front/back). + + + +```kotlin +override fun onCameraFacingChanged(facing: CameraFacing) { + Log.d(TAG, "Camera facing changed to: $facing") + // Update camera switch button state + when (facing) { + CameraFacing.FRONT -> updateCameraIcon(R.drawable.ic_camera_front) + CameraFacing.BACK -> updateCameraIcon(R.drawable.ic_camera_back) + } +} +``` + + +```java +@Override +public void onCameraFacingChanged(CameraFacing facing) { + Log.d(TAG, "Camera facing changed to: " + facing); + // Update camera switch button state + switch (facing) { + case FRONT: + updateCameraIcon(R.drawable.ic_camera_front); + break; + case BACK: + updateCameraIcon(R.drawable.ic_camera_back); + break; + } +} +``` + + + +**CameraFacing Values:** + +| Value | Description | +|-------|-------------| +| `FRONT` | Front-facing camera (selfie camera) | +| `BACK` | Back-facing camera (main camera) | + +**Use Cases:** +- Update camera switch button icon +- Adjust UI for mirrored/non-mirrored preview +- Track camera state + +--- + +## Complete Example + +Here's a complete example handling all media events: + + + +```kotlin +class CallActivity : AppCompatActivity() { + private lateinit var callSession: CallSession + + private lateinit var muteButton: ImageButton + private lateinit var videoButton: ImageButton + private lateinit var screenShareButton: ImageButton + private lateinit var audioModeButton: ImageButton + private lateinit var recordingIndicator: View + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + initViews() + callSession = CallSession.getInstance() + setupMediaEventsListener() + } + + private fun initViews() { + muteButton = findViewById(R.id.muteButton) + videoButton = findViewById(R.id.videoButton) + screenShareButton = findViewById(R.id.screenShareButton) + audioModeButton = findViewById(R.id.audioModeButton) + recordingIndicator = findViewById(R.id.recordingIndicator) + } + + private fun setupMediaEventsListener() { + callSession.addMediaEventsListener(this, object : MediaEventsListener() { + override fun onAudioMuted() { + runOnUiThread { + muteButton.setImageResource(R.drawable.ic_mic_off) + muteButton.isSelected = true + } + } + + override fun onAudioUnMuted() { + runOnUiThread { + muteButton.setImageResource(R.drawable.ic_mic_on) + muteButton.isSelected = false + } + } + + override fun onVideoPaused() { + runOnUiThread { + videoButton.setImageResource(R.drawable.ic_videocam_off) + videoButton.isSelected = true + } + } + + override fun onVideoResumed() { + runOnUiThread { + videoButton.setImageResource(R.drawable.ic_videocam_on) + videoButton.isSelected = false + } + } + + override fun onRecordingStarted() { + runOnUiThread { + recordingIndicator.visibility = View.VISIBLE + Toast.makeText( + this@CallActivity, + "Recording started", + Toast.LENGTH_SHORT + ).show() + } + } + + override fun onRecordingStopped() { + runOnUiThread { + recordingIndicator.visibility = View.GONE + Toast.makeText( + this@CallActivity, + "Recording stopped", + Toast.LENGTH_SHORT + ).show() + } + } + + override fun onScreenShareStarted() { + runOnUiThread { + screenShareButton.isSelected = true + Toast.makeText( + this@CallActivity, + "You are sharing your screen", + Toast.LENGTH_SHORT + ).show() + } + } + + override fun onScreenShareStopped() { + runOnUiThread { + screenShareButton.isSelected = false + } + } + + override fun onAudioModeChanged(audioMode: AudioMode) { + runOnUiThread { + val iconRes = when (audioMode) { + AudioMode.SPEAKER -> R.drawable.ic_volume_up + AudioMode.EARPIECE -> R.drawable.ic_phone_in_talk + AudioMode.BLUETOOTH -> R.drawable.ic_bluetooth_audio + } + audioModeButton.setImageResource(iconRes) + } + } + + override fun onCameraFacingChanged(facing: CameraFacing) { + runOnUiThread { + Log.d(TAG, "Camera switched to: $facing") + } + } + }) + } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + private static final String TAG = "CallActivity"; + private CallSession callSession; + + private ImageButton muteButton; + private ImageButton videoButton; + private ImageButton screenShareButton; + private ImageButton audioModeButton; + private View recordingIndicator; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + initViews(); + callSession = CallSession.getInstance(); + setupMediaEventsListener(); + } + + private void initViews() { + muteButton = findViewById(R.id.muteButton); + videoButton = findViewById(R.id.videoButton); + screenShareButton = findViewById(R.id.screenShareButton); + audioModeButton = findViewById(R.id.audioModeButton); + recordingIndicator = findViewById(R.id.recordingIndicator); + } + + private void setupMediaEventsListener() { + callSession.addMediaEventsListener(this, new MediaEventsListener() { + @Override + public void onAudioMuted() { + runOnUiThread(() -> { + muteButton.setImageResource(R.drawable.ic_mic_off); + muteButton.setSelected(true); + }); + } + + @Override + public void onAudioUnMuted() { + runOnUiThread(() -> { + muteButton.setImageResource(R.drawable.ic_mic_on); + muteButton.setSelected(false); + }); + } + + @Override + public void onVideoPaused() { + runOnUiThread(() -> { + videoButton.setImageResource(R.drawable.ic_videocam_off); + videoButton.setSelected(true); + }); + } + + @Override + public void onVideoResumed() { + runOnUiThread(() -> { + videoButton.setImageResource(R.drawable.ic_videocam_on); + videoButton.setSelected(false); + }); + } + + @Override + public void onRecordingStarted() { + runOnUiThread(() -> { + recordingIndicator.setVisibility(View.VISIBLE); + Toast.makeText( + CallActivity.this, + "Recording started", + Toast.LENGTH_SHORT + ).show(); + }); + } + + @Override + public void onRecordingStopped() { + runOnUiThread(() -> { + recordingIndicator.setVisibility(View.GONE); + Toast.makeText( + CallActivity.this, + "Recording stopped", + Toast.LENGTH_SHORT + ).show(); + }); + } + + @Override + public void onScreenShareStarted() { + runOnUiThread(() -> { + screenShareButton.setSelected(true); + Toast.makeText( + CallActivity.this, + "You are sharing your screen", + Toast.LENGTH_SHORT + ).show(); + }); + } + + @Override + public void onScreenShareStopped() { + runOnUiThread(() -> screenShareButton.setSelected(false)); + } + + @Override + public void onAudioModeChanged(AudioMode audioMode) { + runOnUiThread(() -> { + int iconRes; + switch (audioMode) { + case SPEAKER: + iconRes = R.drawable.ic_volume_up; + break; + case EARPIECE: + iconRes = R.drawable.ic_phone_in_talk; + break; + case BLUETOOTH: + iconRes = R.drawable.ic_bluetooth_audio; + break; + default: + iconRes = R.drawable.ic_volume_up; + } + audioModeButton.setImageResource(iconRes); + }); + } + + @Override + public void onCameraFacingChanged(CameraFacing facing) { + runOnUiThread(() -> Log.d(TAG, "Camera switched to: " + facing)); + } + }); + } +} +``` + + + +--- + +## Callbacks Summary + +| Callback | Parameter | Description | +|----------|-----------|-------------| +| `onAudioMuted` | - | Local audio was muted | +| `onAudioUnMuted` | - | Local audio was unmuted | +| `onVideoPaused` | - | Local video was paused | +| `onVideoResumed` | - | Local video was resumed | +| `onRecordingStarted` | - | Session recording started | +| `onRecordingStopped` | - | Session recording stopped | +| `onScreenShareStarted` | - | Local screen sharing started | +| `onScreenShareStopped` | - | Local screen sharing stopped | +| `onAudioModeChanged` | `AudioMode` | Audio output mode changed | +| `onCameraFacingChanged` | `CameraFacing` | Camera facing changed | + +## Next Steps + + + + Handle UI button click events + + + Control audio programmatically + + diff --git a/calls/android/overview.mdx b/calls/android/overview.mdx new file mode 100644 index 00000000..dffd190d --- /dev/null +++ b/calls/android/overview.mdx @@ -0,0 +1,97 @@ +--- +title: "Calls SDK" +sidebarTitle: "Overview" +--- + +The CometChat Calls SDK enables real-time voice and video calling capabilities in your Android application. Built on top of WebRTC, it provides a complete calling solution with built-in UI components and extensive customization options. + + +Using CometChat UI Kits? Calling is already integrated. See [Android UI Kit](/ui-kit/android/call-features) for a faster integration path. + + +## Prerequisites + +Before integrating the Calls SDK, ensure you have: + +1. **CometChat Account**: [Sign up](https://app.cometchat.com/signup) and create an app to get your App ID, Region, and API Key +2. **CometChat Users**: Users must exist in CometChat to use calling features. For testing, create users via the [Dashboard](https://app.cometchat.com) or [REST API](/rest-api/chat-apis/users/create-user). Authentication is handled by the Calls SDK - see [Authentication](/calls/android/authentication) +3. **Android Requirements**: + - Minimum SDK: API Level 24 (Android 7.0) + - AndroidX compatibility +4. **Permissions**: Camera and microphone permissions for video/audio calls + +## Call Flow + +```mermaid +sequenceDiagram + participant App + participant CometChatCalls + participant CallSession + + App->>CometChatCalls: init() + App->>CometChatCalls: login() + App->>CometChatCalls: generateToken() + App->>CometChatCalls: joinSession() + CometChatCalls-->>App: CallSession + App->>CallSession: Actions (mute, pause, etc.) + CallSession-->>App: Event callbacks + App->>CallSession: leaveSession() +``` + +## Features + + + + + Incoming and outgoing call notifications with accept/reject functionality + + + + Tile and Spotlight view modes for different call scenarios + + + + Switch between speaker, earpiece, Bluetooth, and headphones + + + + Record call sessions for later playback + + + + Retrieve call history and details + + + + Mute, pin, and manage call participants + + + + View screen shares from web participants + + + + Continue calls while using other apps + + + + Signal to get attention during calls + + + + Automatic session termination when alone in a call + + + + +## Architecture + +The SDK is organized around these core components: + +| Component | Description | +|-----------|-------------| +| `CometChatCalls` | Main entry point for SDK initialization, authentication, and session management | +| `CallAppSettings` | Configuration for SDK initialization (App ID, Region) | +| `SessionSettings` | Configuration for individual call sessions | +| `CallSession` | Singleton that manages the active call and provides control methods | +| `Listeners` | Event interfaces for session, participant, media, and UI events | diff --git a/calls/android/participant-actions.mdx b/calls/android/participant-actions.mdx new file mode 100644 index 00000000..10e6c3a9 --- /dev/null +++ b/calls/android/participant-actions.mdx @@ -0,0 +1,353 @@ +--- +title: "Participant Actions" +sidebarTitle: "Participant Actions" +--- + +Manage other participants during an active call session. These methods allow you to mute participants, pause their video, and pin/unpin them in the call layout. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance +- Appropriate permissions (typically host/moderator privileges) + +## Get CallSession Instance + +Participant action methods are called on the `CallSession` singleton: + + + +```kotlin +val callSession = CallSession.getInstance() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +``` + + + +--- + +## Mute Participant + +Mute a specific participant's audio. This prevents other participants from hearing them. + + + +```kotlin +val participantId = "participant_uid" +callSession.muteParticipant(participantId) +``` + + +```java +String participantId = "participant_uid"; +callSession.muteParticipant(participantId); +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `participantId` | String | The unique identifier of the participant to mute | + + +When a participant is muted, all participants receive the `onParticipantAudioMuted(Participant)` callback on their `ParticipantEventListener`. + + +--- + +## Pause Participant Video + +Pause a specific participant's video feed. Other participants will see a placeholder instead of their video. + + + +```kotlin +val participantId = "participant_uid" +callSession.pauseParticipantVideo(participantId) +``` + + +```java +String participantId = "participant_uid"; +callSession.pauseParticipantVideo(participantId); +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `participantId` | String | The unique identifier of the participant whose video to pause | + + +When a participant's video is paused, all participants receive the `onParticipantVideoPaused(Participant)` callback on their `ParticipantEventListener`. + + +--- + +## Pin Participant + +Pin a participant to keep them prominently displayed in the call layout, regardless of who is speaking. + + + +```kotlin +callSession.pinParticipant() +``` + + +```java +callSession.pinParticipant(); +``` + + + + +Pinning is particularly useful in Spotlight layout mode where you want to keep a specific participant in focus. + + +--- + +## Unpin Participant + +Remove the pin from a participant, returning to the default layout behavior. + + + +```kotlin +callSession.unPinParticipant() +``` + + +```java +callSession.unPinParticipant(); +``` + + + +--- + +## Listen for Participant Events + +Register a `ParticipantEventListener` to receive callbacks when participant states change: + + + +```kotlin +callSession.addParticipantEventListener(this, object : ParticipantEventListener { + override fun onParticipantJoined(participant: Participant) { + Log.d(TAG, "${participant.name} joined the call") + } + + override fun onParticipantLeft(participant: Participant) { + Log.d(TAG, "${participant.name} left the call") + } + + override fun onParticipantAudioMuted(participant: Participant) { + Log.d(TAG, "${participant.name} was muted") + } + + override fun onParticipantAudioUnmuted(participant: Participant) { + Log.d(TAG, "${participant.name} was unmuted") + } + + override fun onParticipantVideoPaused(participant: Participant) { + Log.d(TAG, "${participant.name}'s video was paused") + } + + override fun onParticipantVideoResumed(participant: Participant) { + Log.d(TAG, "${participant.name}'s video was resumed") + } + + override fun onParticipantListChanged(participants: List) { + Log.d(TAG, "Participant list updated: ${participants.size} participants") + // Update your participant list UI + } + + override fun onDominantSpeakerChanged(participant: Participant) { + Log.d(TAG, "Dominant speaker: ${participant.name}") + } + + // Other callbacks... + override fun onParticipantHandRaised(participant: Participant) {} + override fun onParticipantHandLowered(participant: Participant) {} + override fun onParticipantStartedScreenShare(participant: Participant) {} + override fun onParticipantStoppedScreenShare(participant: Participant) {} + override fun onParticipantStartedRecording(participant: Participant) {} + override fun onParticipantStoppedRecording(participant: Participant) {} +}) +``` + + +```java +callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantJoined(Participant participant) { + Log.d(TAG, participant.getName() + " joined the call"); + } + + @Override + public void onParticipantLeft(Participant participant) { + Log.d(TAG, participant.getName() + " left the call"); + } + + @Override + public void onParticipantAudioMuted(Participant participant) { + Log.d(TAG, participant.getName() + " was muted"); + } + + @Override + public void onParticipantAudioUnmuted(Participant participant) { + Log.d(TAG, participant.getName() + " was unmuted"); + } + + @Override + public void onParticipantVideoPaused(Participant participant) { + Log.d(TAG, participant.getName() + "'s video was paused"); + } + + @Override + public void onParticipantVideoResumed(Participant participant) { + Log.d(TAG, participant.getName() + "'s video was resumed"); + } + + @Override + public void onParticipantListChanged(List participants) { + Log.d(TAG, "Participant list updated: " + participants.size() + " participants"); + // Update your participant list UI + } + + @Override + public void onDominantSpeakerChanged(Participant participant) { + Log.d(TAG, "Dominant speaker: " + participant.getName()); + } + + // Other callbacks... + @Override + public void onParticipantHandRaised(Participant participant) {} + @Override + public void onParticipantHandLowered(Participant participant) {} + @Override + public void onParticipantStartedScreenShare(Participant participant) {} + @Override + public void onParticipantStoppedScreenShare(Participant participant) {} + @Override + public void onParticipantStartedRecording(Participant participant) {} + @Override + public void onParticipantStoppedRecording(Participant participant) {} +}); +``` + + + +--- + +## Participant Object + +The `Participant` object contains information about a call participant: + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | String | Unique identifier of the participant | +| `name` | String | Display name of the participant | +| `avatar` | String | URL of the participant's avatar image | +| `isAudioMuted` | Boolean | Whether the participant's audio is muted | +| `isVideoPaused` | Boolean | Whether the participant's video is paused | + +--- + +## Show/Hide Participant List Button + +Control the visibility of the participant list button in the call UI: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideParticipantListButton(false) // Show the participant list button + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideParticipantListButton(false) // Show the participant list button + .build(); +``` + + + +--- + +## Participant List Button Click Listener + +Listen for when users tap the participant list button: + + + +```kotlin +callSession.addButtonClickListener(this, object : ButtonClickListener { + override fun onParticipantListButtonClicked() { + Log.d(TAG, "Participant list button clicked") + // Show custom participant list UI if needed + } + + // Other ButtonClickListener callbacks... + override fun onLeaveSessionButtonClicked() {} + override fun onRaiseHandButtonClicked() {} + override fun onShareInviteButtonClicked() {} + override fun onChangeLayoutButtonClicked() {} + override fun onToggleAudioButtonClicked() {} + override fun onToggleVideoButtonClicked() {} + override fun onSwitchCameraButtonClicked() {} + override fun onChatButtonClicked() {} + override fun onRecordingToggleButtonClicked() {} +}) +``` + + +```java +callSession.addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onParticipantListButtonClicked() { + Log.d(TAG, "Participant list button clicked"); + // Show custom participant list UI if needed + } + + // Other ButtonClickListener callbacks... + @Override + public void onLeaveSessionButtonClicked() {} + @Override + public void onRaiseHandButtonClicked() {} + @Override + public void onShareInviteButtonClicked() {} + @Override + public void onChangeLayoutButtonClicked() {} + @Override + public void onToggleAudioButtonClicked() {} + @Override + public void onToggleVideoButtonClicked() {} + @Override + public void onSwitchCameraButtonClicked() {} + @Override + public void onChatButtonClicked() {} + @Override + public void onRecordingToggleButtonClicked() {} +}); +``` + + + +## Next Steps + + + + Control call layout and UI elements + + + Handle all participant events + + diff --git a/calls/android/participant-event-listener.mdx b/calls/android/participant-event-listener.mdx new file mode 100644 index 00000000..ae878872 --- /dev/null +++ b/calls/android/participant-event-listener.mdx @@ -0,0 +1,758 @@ +--- +title: "Participant Event Listener" +sidebarTitle: "Participant Event Listener" +--- + +Monitor participant activities with `ParticipantEventListener`. This listener provides callbacks for participant join/leave events, audio/video state changes, hand raise actions, screen sharing, recording, and more. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance + +## Register Listener + +Register a `ParticipantEventListener` to receive participant event callbacks: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantJoined(participant: Participant) { + Log.d(TAG, "${participant.name} joined the call") + } + + override fun onParticipantLeft(participant: Participant) { + Log.d(TAG, "${participant.name} left the call") + } + + override fun onParticipantListChanged(participants: List) { + Log.d(TAG, "Participant list updated: ${participants.size} participants") + } + + // Additional callbacks... +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantJoined(Participant participant) { + Log.d(TAG, participant.getName() + " joined the call"); + } + + @Override + public void onParticipantLeft(Participant participant) { + Log.d(TAG, participant.getName() + " left the call"); + } + + @Override + public void onParticipantListChanged(List participants) { + Log.d(TAG, "Participant list updated: " + participants.size() + " participants"); + } + + // Additional callbacks... +}); +``` + + + + +The listener is automatically removed when the `LifecycleOwner` (Activity/Fragment) is destroyed, preventing memory leaks. + + +--- + +## Participant Object + +The `Participant` object contains information about a call participant: + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | `String` | Unique identifier of the participant | +| `name` | `String` | Display name of the participant | +| `avatar` | `String` | Avatar URL of the participant | +| `isAudioMuted` | `Boolean` | Whether audio is muted | +| `isVideoPaused` | `Boolean` | Whether video is paused | +| `isHandRaised` | `Boolean` | Whether hand is raised | +| `isScreenSharing` | `Boolean` | Whether screen is being shared | + +--- + +## Callbacks + +### onParticipantJoined + +Triggered when a new participant joins the call. + + + +```kotlin +override fun onParticipantJoined(participant: Participant) { + Log.d(TAG, "${participant.name} joined the call") + // Show join notification + // Update participant grid +} +``` + + +```java +@Override +public void onParticipantJoined(Participant participant) { + Log.d(TAG, participant.getName() + " joined the call"); + // Show join notification + // Update participant grid +} +``` + + + +**Use Cases:** +- Display join notification/toast +- Update participant count +- Play join sound + +--- + +### onParticipantLeft + +Triggered when a participant leaves the call. + + + +```kotlin +override fun onParticipantLeft(participant: Participant) { + Log.d(TAG, "${participant.name} left the call") + // Show leave notification + // Update participant grid +} +``` + + +```java +@Override +public void onParticipantLeft(Participant participant) { + Log.d(TAG, participant.getName() + " left the call"); + // Show leave notification + // Update participant grid +} +``` + + + +**Use Cases:** +- Display leave notification +- Update participant count +- Play leave sound + +--- + +### onParticipantListChanged + +Triggered when the participant list changes (join, leave, or any update). + + + +```kotlin +override fun onParticipantListChanged(participants: List) { + Log.d(TAG, "Participants: ${participants.size}") + // Update participant list UI + updateParticipantGrid(participants) +} +``` + + +```java +@Override +public void onParticipantListChanged(List participants) { + Log.d(TAG, "Participants: " + participants.size()); + // Update participant list UI + updateParticipantGrid(participants); +} +``` + + + +**Use Cases:** +- Refresh participant list/grid +- Update participant count badge +- Sync local state with server + +--- + +### onParticipantAudioMuted + +Triggered when a participant mutes their audio. + + + +```kotlin +override fun onParticipantAudioMuted(participant: Participant) { + Log.d(TAG, "${participant.name} muted their audio") + // Show muted indicator on participant tile +} +``` + + +```java +@Override +public void onParticipantAudioMuted(Participant participant) { + Log.d(TAG, participant.getName() + " muted their audio"); + // Show muted indicator on participant tile +} +``` + + + +--- + +### onParticipantAudioUnmuted + +Triggered when a participant unmutes their audio. + + + +```kotlin +override fun onParticipantAudioUnmuted(participant: Participant) { + Log.d(TAG, "${participant.name} unmuted their audio") + // Hide muted indicator on participant tile +} +``` + + +```java +@Override +public void onParticipantAudioUnmuted(Participant participant) { + Log.d(TAG, participant.getName() + " unmuted their audio"); + // Hide muted indicator on participant tile +} +``` + + + +--- + +### onParticipantVideoPaused + +Triggered when a participant pauses their video. + + + +```kotlin +override fun onParticipantVideoPaused(participant: Participant) { + Log.d(TAG, "${participant.name} paused their video") + // Show avatar or placeholder instead of video +} +``` + + +```java +@Override +public void onParticipantVideoPaused(Participant participant) { + Log.d(TAG, participant.getName() + " paused their video"); + // Show avatar or placeholder instead of video +} +``` + + + +--- + +### onParticipantVideoResumed + +Triggered when a participant resumes their video. + + + +```kotlin +override fun onParticipantVideoResumed(participant: Participant) { + Log.d(TAG, "${participant.name} resumed their video") + // Show video stream +} +``` + + +```java +@Override +public void onParticipantVideoResumed(Participant participant) { + Log.d(TAG, participant.getName() + " resumed their video"); + // Show video stream +} +``` + + + +--- + +### onParticipantHandRaised + +Triggered when a participant raises their hand. + + + +```kotlin +override fun onParticipantHandRaised(participant: Participant) { + Log.d(TAG, "${participant.name} raised their hand") + // Show hand raised indicator + // Optionally play notification sound +} +``` + + +```java +@Override +public void onParticipantHandRaised(Participant participant) { + Log.d(TAG, participant.getName() + " raised their hand"); + // Show hand raised indicator + // Optionally play notification sound +} +``` + + + +--- + +### onParticipantHandLowered + +Triggered when a participant lowers their hand. + + + +```kotlin +override fun onParticipantHandLowered(participant: Participant) { + Log.d(TAG, "${participant.name} lowered their hand") + // Hide hand raised indicator +} +``` + + +```java +@Override +public void onParticipantHandLowered(Participant participant) { + Log.d(TAG, participant.getName() + " lowered their hand"); + // Hide hand raised indicator +} +``` + + + +--- + +### onParticipantStartedScreenShare + +Triggered when a participant starts sharing their screen. + + + +```kotlin +override fun onParticipantStartedScreenShare(participant: Participant) { + Log.d(TAG, "${participant.name} started screen sharing") + // Switch to screen share view + // Show screen share indicator +} +``` + + +```java +@Override +public void onParticipantStartedScreenShare(Participant participant) { + Log.d(TAG, participant.getName() + " started screen sharing"); + // Switch to screen share view + // Show screen share indicator +} +``` + + + +--- + +### onParticipantStoppedScreenShare + +Triggered when a participant stops sharing their screen. + + + +```kotlin +override fun onParticipantStoppedScreenShare(participant: Participant) { + Log.d(TAG, "${participant.name} stopped screen sharing") + // Switch back to normal view + // Hide screen share indicator +} +``` + + +```java +@Override +public void onParticipantStoppedScreenShare(Participant participant) { + Log.d(TAG, participant.getName() + " stopped screen sharing"); + // Switch back to normal view + // Hide screen share indicator +} +``` + + + +--- + +### onParticipantStartedRecording + +Triggered when a participant starts recording the session. + + + +```kotlin +override fun onParticipantStartedRecording(participant: Participant) { + Log.d(TAG, "${participant.name} started recording") + // Show recording indicator + // Notify other participants +} +``` + + +```java +@Override +public void onParticipantStartedRecording(Participant participant) { + Log.d(TAG, participant.getName() + " started recording"); + // Show recording indicator + // Notify other participants +} +``` + + + +--- + +### onParticipantStoppedRecording + +Triggered when a participant stops recording the session. + + + +```kotlin +override fun onParticipantStoppedRecording(participant: Participant) { + Log.d(TAG, "${participant.name} stopped recording") + // Hide recording indicator +} +``` + + +```java +@Override +public void onParticipantStoppedRecording(Participant participant) { + Log.d(TAG, participant.getName() + " stopped recording"); + // Hide recording indicator +} +``` + + + +--- + +### onDominantSpeakerChanged + +Triggered when the dominant speaker changes (the participant currently speaking the loudest). + + + +```kotlin +override fun onDominantSpeakerChanged(participant: Participant) { + Log.d(TAG, "${participant.name} is now the dominant speaker") + // Highlight the dominant speaker's tile + // Auto-focus on dominant speaker in spotlight mode +} +``` + + +```java +@Override +public void onDominantSpeakerChanged(Participant participant) { + Log.d(TAG, participant.getName() + " is now the dominant speaker"); + // Highlight the dominant speaker's tile + // Auto-focus on dominant speaker in spotlight mode +} +``` + + + +**Use Cases:** +- Highlight active speaker in UI +- Auto-switch spotlight to dominant speaker +- Show speaking indicator animation + +--- + +## Complete Example + +Here's a complete example handling all participant events: + + + +```kotlin +class CallActivity : AppCompatActivity() { + private lateinit var callSession: CallSession + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + callSession = CallSession.getInstance() + setupParticipantEventListener() + } + + private fun setupParticipantEventListener() { + callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantJoined(participant: Participant) { + runOnUiThread { + showToast("${participant.name} joined") + } + } + + override fun onParticipantLeft(participant: Participant) { + runOnUiThread { + showToast("${participant.name} left") + } + } + + override fun onParticipantListChanged(participants: List) { + runOnUiThread { + updateParticipantCount(participants.size) + } + } + + override fun onParticipantAudioMuted(participant: Participant) { + runOnUiThread { + updateParticipantAudioState(participant.uid, isMuted = true) + } + } + + override fun onParticipantAudioUnmuted(participant: Participant) { + runOnUiThread { + updateParticipantAudioState(participant.uid, isMuted = false) + } + } + + override fun onParticipantVideoPaused(participant: Participant) { + runOnUiThread { + updateParticipantVideoState(participant.uid, isPaused = true) + } + } + + override fun onParticipantVideoResumed(participant: Participant) { + runOnUiThread { + updateParticipantVideoState(participant.uid, isPaused = false) + } + } + + override fun onParticipantHandRaised(participant: Participant) { + runOnUiThread { + showHandRaisedIndicator(participant.uid, isRaised = true) + } + } + + override fun onParticipantHandLowered(participant: Participant) { + runOnUiThread { + showHandRaisedIndicator(participant.uid, isRaised = false) + } + } + + override fun onParticipantStartedScreenShare(participant: Participant) { + runOnUiThread { + showScreenShareView(participant) + } + } + + override fun onParticipantStoppedScreenShare(participant: Participant) { + runOnUiThread { + hideScreenShareView() + } + } + + override fun onParticipantStartedRecording(participant: Participant) { + runOnUiThread { + showRecordingIndicator(true) + } + } + + override fun onParticipantStoppedRecording(participant: Participant) { + runOnUiThread { + showRecordingIndicator(false) + } + } + + override fun onDominantSpeakerChanged(participant: Participant) { + runOnUiThread { + highlightDominantSpeaker(participant.uid) + } + } + }) + } + + // UI helper methods + private fun showToast(message: String) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + } + + private fun updateParticipantCount(count: Int) { /* ... */ } + private fun updateParticipantAudioState(uid: String, isMuted: Boolean) { /* ... */ } + private fun updateParticipantVideoState(uid: String, isPaused: Boolean) { /* ... */ } + private fun showHandRaisedIndicator(uid: String, isRaised: Boolean) { /* ... */ } + private fun showScreenShareView(participant: Participant) { /* ... */ } + private fun hideScreenShareView() { /* ... */ } + private fun showRecordingIndicator(show: Boolean) { /* ... */ } + private fun highlightDominantSpeaker(uid: String) { /* ... */ } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + private static final String TAG = "CallActivity"; + private CallSession callSession; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + callSession = CallSession.getInstance(); + setupParticipantEventListener(); + } + + private void setupParticipantEventListener() { + callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantJoined(Participant participant) { + runOnUiThread(() -> showToast(participant.getName() + " joined")); + } + + @Override + public void onParticipantLeft(Participant participant) { + runOnUiThread(() -> showToast(participant.getName() + " left")); + } + + @Override + public void onParticipantListChanged(List participants) { + runOnUiThread(() -> updateParticipantCount(participants.size())); + } + + @Override + public void onParticipantAudioMuted(Participant participant) { + runOnUiThread(() -> + updateParticipantAudioState(participant.getUid(), true)); + } + + @Override + public void onParticipantAudioUnmuted(Participant participant) { + runOnUiThread(() -> + updateParticipantAudioState(participant.getUid(), false)); + } + + @Override + public void onParticipantVideoPaused(Participant participant) { + runOnUiThread(() -> + updateParticipantVideoState(participant.getUid(), true)); + } + + @Override + public void onParticipantVideoResumed(Participant participant) { + runOnUiThread(() -> + updateParticipantVideoState(participant.getUid(), false)); + } + + @Override + public void onParticipantHandRaised(Participant participant) { + runOnUiThread(() -> + showHandRaisedIndicator(participant.getUid(), true)); + } + + @Override + public void onParticipantHandLowered(Participant participant) { + runOnUiThread(() -> + showHandRaisedIndicator(participant.getUid(), false)); + } + + @Override + public void onParticipantStartedScreenShare(Participant participant) { + runOnUiThread(() -> showScreenShareView(participant)); + } + + @Override + public void onParticipantStoppedScreenShare(Participant participant) { + runOnUiThread(() -> hideScreenShareView()); + } + + @Override + public void onParticipantStartedRecording(Participant participant) { + runOnUiThread(() -> showRecordingIndicator(true)); + } + + @Override + public void onParticipantStoppedRecording(Participant participant) { + runOnUiThread(() -> showRecordingIndicator(false)); + } + + @Override + public void onDominantSpeakerChanged(Participant participant) { + runOnUiThread(() -> highlightDominantSpeaker(participant.getUid())); + } + }); + } + + // UI helper methods + private void showToast(String message) { + Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); + } + + private void updateParticipantCount(int count) { /* ... */ } + private void updateParticipantAudioState(String uid, boolean isMuted) { /* ... */ } + private void updateParticipantVideoState(String uid, boolean isPaused) { /* ... */ } + private void showHandRaisedIndicator(String uid, boolean isRaised) { /* ... */ } + private void showScreenShareView(Participant participant) { /* ... */ } + private void hideScreenShareView() { /* ... */ } + private void showRecordingIndicator(boolean show) { /* ... */ } + private void highlightDominantSpeaker(String uid) { /* ... */ } +} +``` + + + +--- + +## Callbacks Summary + +| Callback | Parameter | Description | +|----------|-----------|-------------| +| `onParticipantJoined` | `Participant` | A participant joined the call | +| `onParticipantLeft` | `Participant` | A participant left the call | +| `onParticipantListChanged` | `List` | Participant list was updated | +| `onParticipantAudioMuted` | `Participant` | A participant muted their audio | +| `onParticipantAudioUnmuted` | `Participant` | A participant unmuted their audio | +| `onParticipantVideoPaused` | `Participant` | A participant paused their video | +| `onParticipantVideoResumed` | `Participant` | A participant resumed their video | +| `onParticipantHandRaised` | `Participant` | A participant raised their hand | +| `onParticipantHandLowered` | `Participant` | A participant lowered their hand | +| `onParticipantStartedScreenShare` | `Participant` | A participant started screen sharing | +| `onParticipantStoppedScreenShare` | `Participant` | A participant stopped screen sharing | +| `onParticipantStartedRecording` | `Participant` | A participant started recording | +| `onParticipantStoppedRecording` | `Participant` | A participant stopped recording | +| `onDominantSpeakerChanged` | `Participant` | The dominant speaker changed | + +## Next Steps + + + + Handle local media state changes + + + Control other participants + + diff --git a/calls/android/participant-management.mdx b/calls/android/participant-management.mdx new file mode 100644 index 00000000..8f961222 --- /dev/null +++ b/calls/android/participant-management.mdx @@ -0,0 +1,244 @@ +--- +title: "Participant Management" +sidebarTitle: "Participant Management" +--- + +Manage participants during a call with actions like muting, pausing video, and pinning. These features help maintain order in group calls and highlight important speakers. + + +By default, all participants who join a call have moderator access and can perform these actions. Implementing role-based moderation (e.g., restricting actions to hosts only) is the responsibility of the app developer based on their use case. + + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- User [logged in](/calls/android/authentication) +- An active [call session](/calls/android/join-session) + +## Mute a Participant + +Mute a specific participant's audio. This affects the participant for all users in the call. + + + +```kotlin +val callSession = CallSession.getInstance() +callSession.muteParticipant(participant.uid) +``` + + +```java +CallSession callSession = CallSession.getInstance(); +callSession.muteParticipant(participant.getUid()); +``` + + + +## Pause Participant Video + +Pause a specific participant's video. This affects the participant for all users in the call. + + + +```kotlin +val callSession = CallSession.getInstance() +callSession.pauseParticipantVideo(participant.uid) +``` + + +```java +CallSession callSession = CallSession.getInstance(); +callSession.pauseParticipantVideo(participant.getUid()); +``` + + + +## Pin a Participant + +Pin a participant to keep them prominently displayed regardless of who is speaking. Useful for keeping focus on a presenter or important speaker. + + + +```kotlin +val callSession = CallSession.getInstance() + +// Pin a participant +callSession.pinParticipant(participant.uid) + +// Unpin (returns to automatic speaker highlighting) +callSession.unPinParticipant() +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +// Pin a participant +callSession.pinParticipant(participant.getUid()); + +// Unpin (returns to automatic speaker highlighting) +callSession.unPinParticipant(); +``` + + + + +Pinning a participant only affects your local view. Other participants can pin different users independently. + + +## Listen for Participant Events + +Monitor participant state changes using `ParticipantEventListener`: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantJoined(participant: Participant) { + Log.d(TAG, "${participant.name} joined the call") + updateParticipantList() + } + + override fun onParticipantLeft(participant: Participant) { + Log.d(TAG, "${participant.name} left the call") + updateParticipantList() + } + + override fun onParticipantListChanged(participants: List) { + Log.d(TAG, "Participant count: ${participants.size}") + refreshParticipantList(participants) + } + + override fun onParticipantAudioMuted(participant: Participant) { + Log.d(TAG, "${participant.name} was muted") + updateMuteIndicator(participant, muted = true) + } + + override fun onParticipantAudioUnmuted(participant: Participant) { + Log.d(TAG, "${participant.name} was unmuted") + updateMuteIndicator(participant, muted = false) + } + + override fun onParticipantVideoPaused(participant: Participant) { + Log.d(TAG, "${participant.name} video paused") + showParticipantAvatar(participant) + } + + override fun onParticipantVideoResumed(participant: Participant) { + Log.d(TAG, "${participant.name} video resumed") + showParticipantVideo(participant) + } + + override fun onDominantSpeakerChanged(participant: Participant) { + Log.d(TAG, "${participant.name} is now speaking") + highlightActiveSpeaker(participant) + } + + // Other callbacks... + override fun onParticipantHandRaised(participant: Participant) {} + override fun onParticipantHandLowered(participant: Participant) {} + override fun onParticipantStartedScreenShare(participant: Participant) {} + override fun onParticipantStoppedScreenShare(participant: Participant) {} + override fun onParticipantStartedRecording(participant: Participant) {} + override fun onParticipantStoppedRecording(participant: Participant) {} +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantJoined(Participant participant) { + Log.d(TAG, participant.getName() + " joined the call"); + updateParticipantList(); + } + + @Override + public void onParticipantLeft(Participant participant) { + Log.d(TAG, participant.getName() + " left the call"); + updateParticipantList(); + } + + @Override + public void onParticipantListChanged(List participants) { + Log.d(TAG, "Participant count: " + participants.size()); + refreshParticipantList(participants); + } + + @Override + public void onParticipantAudioMuted(Participant participant) { + Log.d(TAG, participant.getName() + " was muted"); + updateMuteIndicator(participant, true); + } + + @Override + public void onParticipantAudioUnmuted(Participant participant) { + Log.d(TAG, participant.getName() + " was unmuted"); + updateMuteIndicator(participant, false); + } + + @Override + public void onParticipantVideoPaused(Participant participant) { + Log.d(TAG, participant.getName() + " video paused"); + showParticipantAvatar(participant); + } + + @Override + public void onParticipantVideoResumed(Participant participant) { + Log.d(TAG, participant.getName() + " video resumed"); + showParticipantVideo(participant); + } + + @Override + public void onDominantSpeakerChanged(Participant participant) { + Log.d(TAG, participant.getName() + " is now speaking"); + highlightActiveSpeaker(participant); + } + + // Other callbacks... +}); +``` + + + +## Participant Object + +The `Participant` object contains information about each call participant: + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | String | Unique identifier (CometChat user ID) | +| `name` | String | Display name | +| `avatar` | String | URL of avatar image | +| `pid` | String | Participant ID for this call session | +| `role` | String | Role in the call | +| `audioMuted` | Boolean | Whether audio is muted | +| `videoPaused` | Boolean | Whether video is paused | +| `isPinned` | Boolean | Whether pinned in layout | +| `isPresenting` | Boolean | Whether screen sharing | +| `raisedHandTimestamp` | Long | Timestamp when hand was raised (0 if not raised) | + +## Hide Participant List Button + +To hide the participant list button in the call UI: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideParticipantListButton(true) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideParticipantListButton(true) + .build(); +``` + + diff --git a/calls/android/picture-in-picture.mdx b/calls/android/picture-in-picture.mdx new file mode 100644 index 00000000..c9302e27 --- /dev/null +++ b/calls/android/picture-in-picture.mdx @@ -0,0 +1,204 @@ +--- +title: "Picture-in-Picture" +sidebarTitle: "Picture-in-Picture" +--- + +Enable Picture-in-Picture (PiP) mode to allow users to continue their call in a floating window while using other apps. PiP provides a seamless multitasking experience during calls. + + +Picture-in-Picture implementation is handled at the app level using Android's PiP APIs. The Calls SDK only adjusts the call UI layout to fit the PiP window - it does not manage the PiP window itself. + + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- User [logged in](/calls/android/authentication) +- An active [call session](/calls/android/join-session) +- Android 8.0 (API level 26) or higher + +## How It Works + +1. Your app enters PiP mode using Android's `enterPictureInPictureMode()` API +2. You notify the Calls SDK by calling `enablePictureInPictureLayout()` +3. The SDK adjusts the call UI to fit the smaller PiP window (hides controls, optimizes layout) +4. When exiting PiP, call `disablePictureInPictureLayout()` to restore the full UI + +## Enable Picture-in-Picture + +Enter PiP mode programmatically using the `enablePictureInPictureLayout()` action: + + + +```kotlin +val callSession = CallSession.getInstance() +callSession.enablePictureInPictureLayout() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +callSession.enablePictureInPictureLayout(); +``` + + + +## Disable Picture-in-Picture + +Exit PiP mode and return to the full-screen call interface: + + + +```kotlin +val callSession = CallSession.getInstance() +callSession.disablePictureInPictureLayout() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +callSession.disablePictureInPictureLayout(); +``` + + + +## Listen for PiP Events + +Monitor PiP mode transitions using `LayoutListener` to update your UI accordingly: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addLayoutListener(this, object : LayoutListener() { + override fun onPictureInPictureLayoutEnabled() { + Log.d(TAG, "Entered PiP mode") + // Hide custom overlays or controls + hideCustomControls() + } + + override fun onPictureInPictureLayoutDisabled() { + Log.d(TAG, "Exited PiP mode") + // Show custom overlays or controls + showCustomControls() + } + + override fun onCallLayoutChanged(layoutType: LayoutType) {} + override fun onParticipantListVisible() {} + override fun onParticipantListHidden() {} +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addLayoutListener(this, new LayoutListener() { + @Override + public void onPictureInPictureLayoutEnabled() { + Log.d(TAG, "Entered PiP mode"); + // Hide custom overlays or controls + hideCustomControls(); + } + + @Override + public void onPictureInPictureLayoutDisabled() { + Log.d(TAG, "Exited PiP mode"); + // Show custom overlays or controls + showCustomControls(); + } + + @Override public void onCallLayoutChanged(LayoutType layoutType) {} + @Override public void onParticipantListVisible() {} + @Override public void onParticipantListHidden() {} +}); +``` + + + +## Auto-Enter PiP on Home Press + +To automatically enter PiP mode when the user presses the home button or navigates away, override `onUserLeaveHint()` in your Activity: + + + +```kotlin +class CallActivity : AppCompatActivity() { + + override fun onUserLeaveHint() { + super.onUserLeaveHint() + if (CallSession.getInstance().isSessionActive()) { + // Enter Android PiP mode + enterPictureInPictureMode( + PictureInPictureParams.Builder() + .setAspectRatio(Rational(16, 9)) + .build() + ) + // Notify SDK to adjust layout + CallSession.getInstance().enablePictureInPictureLayout() + } + } + + override fun onPictureInPictureModeChanged(isInPiPMode: Boolean, config: Configuration) { + super.onPictureInPictureModeChanged(isInPiPMode, config) + if (isInPiPMode) { + CallSession.getInstance().enablePictureInPictureLayout() + } else { + CallSession.getInstance().disablePictureInPictureLayout() + } + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + + @Override + protected void onUserLeaveHint() { + super.onUserLeaveHint(); + if (CallSession.getInstance().isSessionActive()) { + // Enter Android PiP mode + enterPictureInPictureMode( + new PictureInPictureParams.Builder() + .setAspectRatio(new Rational(16, 9)) + .build() + ); + // Notify SDK to adjust layout + CallSession.getInstance().enablePictureInPictureLayout(); + } + } + + @Override + public void onPictureInPictureModeChanged(boolean isInPiPMode, Configuration config) { + super.onPictureInPictureModeChanged(isInPiPMode, config); + if (isInPiPMode) { + CallSession.getInstance().enablePictureInPictureLayout(); + } else { + CallSession.getInstance().disablePictureInPictureLayout(); + } + } +} +``` + + + +## Android Manifest Configuration + +Add PiP support to your Activity in `AndroidManifest.xml`: + +```xml + +``` + +| Attribute | Description | +|-----------|-------------| +| `supportsPictureInPicture` | Enables PiP support for the activity | +| `configChanges` | Prevents activity restart during PiP transitions | + + +PiP mode is only available on Android 8.0 (API level 26) and higher. On older devices, the PiP actions will have no effect. + diff --git a/calls/android/raise-hand.mdx b/calls/android/raise-hand.mdx new file mode 100644 index 00000000..1fd7cb27 --- /dev/null +++ b/calls/android/raise-hand.mdx @@ -0,0 +1,173 @@ +--- +title: "Raise Hand" +sidebarTitle: "Raise Hand" +--- + +Allow participants to raise their hand to get attention during calls. This feature is useful for large meetings, webinars, or any scenario where participants need to signal they want to speak. + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- User [logged in](/calls/android/authentication) +- An active [call session](/calls/android/join-session) + +## Raise Hand + +Signal that you want to speak or get attention: + + + +```kotlin +val callSession = CallSession.getInstance() +callSession.raiseHand() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +callSession.raiseHand(); +``` + + + +## Lower Hand + +Remove the raised hand indicator: + + + +```kotlin +val callSession = CallSession.getInstance() +callSession.lowerHand() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +callSession.lowerHand(); +``` + + + +## Listen for Raise Hand Events + +Monitor when participants raise or lower their hands using `ParticipantEventListener`: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantHandRaised(participant: Participant) { + Log.d(TAG, "${participant.name} raised their hand") + // Show notification or visual indicator + showHandRaisedNotification(participant) + } + + override fun onParticipantHandLowered(participant: Participant) { + Log.d(TAG, "${participant.name} lowered their hand") + // Remove notification or visual indicator + hideHandRaisedIndicator(participant) + } + + // Other callbacks... + override fun onParticipantJoined(participant: Participant) {} + override fun onParticipantLeft(participant: Participant) {} + override fun onParticipantListChanged(participants: List) {} + override fun onParticipantAudioMuted(participant: Participant) {} + override fun onParticipantAudioUnmuted(participant: Participant) {} + override fun onParticipantVideoPaused(participant: Participant) {} + override fun onParticipantVideoResumed(participant: Participant) {} + override fun onParticipantStartedScreenShare(participant: Participant) {} + override fun onParticipantStoppedScreenShare(participant: Participant) {} + override fun onParticipantStartedRecording(participant: Participant) {} + override fun onParticipantStoppedRecording(participant: Participant) {} + override fun onDominantSpeakerChanged(participant: Participant) {} +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantHandRaised(Participant participant) { + Log.d(TAG, participant.getName() + " raised their hand"); + // Show notification or visual indicator + showHandRaisedNotification(participant); + } + + @Override + public void onParticipantHandLowered(Participant participant) { + Log.d(TAG, participant.getName() + " lowered their hand"); + // Remove notification or visual indicator + hideHandRaisedIndicator(participant); + } + + // Other callbacks... +}); +``` + + + +## Check Raised Hand Status + +The `Participant` object includes a `raisedHandTimestamp` property to check if a participant has their hand raised: + + + +```kotlin +callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantListChanged(participants: List) { + val raisedHands = participants.filter { it.raisedHandTimestamp > 0 } + .sortedBy { it.raisedHandTimestamp } + + // Display participants with raised hands in order + updateRaisedHandsList(raisedHands) + } +}) +``` + + +```java +callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantListChanged(List participants) { + List raisedHands = new ArrayList<>(); + for (Participant p : participants) { + if (p.getRaisedHandTimestamp() > 0) { + raisedHands.add(p); + } + } + // Sort by timestamp and display + Collections.sort(raisedHands, (a, b) -> + Long.compare(a.getRaisedHandTimestamp(), b.getRaisedHandTimestamp())); + updateRaisedHandsList(raisedHands); + } +}); +``` + + + +## Hide Raise Hand Button + +To disable the raise hand feature, hide the button in the call UI: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideRaiseHandButton(true) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideRaiseHandButton(true) + .build(); +``` + + diff --git a/calls/android/recording.mdx b/calls/android/recording.mdx new file mode 100644 index 00000000..bf6e77d3 --- /dev/null +++ b/calls/android/recording.mdx @@ -0,0 +1,271 @@ +--- +title: "Recording" +sidebarTitle: "Recording" +--- + +Record call sessions for later playback. Recordings are stored server-side and can be accessed through call logs or the CometChat Dashboard. + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- User [logged in](/calls/android/authentication) +- Recording feature enabled for your CometChat app + + +Recording must be enabled for your CometChat app. Contact support or check your Dashboard settings if recording is not available. + + +## Start Recording + +Start recording during an active call session: + + + +```kotlin +val callSession = CallSession.getInstance() +callSession.startRecording() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +callSession.startRecording(); +``` + + + +All participants are notified when recording starts. + +## Stop Recording + +Stop an active recording: + + + +```kotlin +val callSession = CallSession.getInstance() +callSession.stopRecording() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +callSession.stopRecording(); +``` + + + +## Auto-Start Recording + +Configure calls to automatically start recording when the session begins: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .enableAutoStartRecording(true) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .enableAutoStartRecording(true) + .build(); +``` + + + +## Hide Recording Button + +Hide the recording button from the default call UI: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideRecordingButton(true) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideRecordingButton(true) + .build(); +``` + + + +## Listen for Recording Events + +Monitor recording state changes using `MediaEventsListener`: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addMediaEventsListener(this, object : MediaEventsListener() { + override fun onRecordingStarted() { + Log.d(TAG, "Recording started") + showRecordingIndicator() + } + + override fun onRecordingStopped() { + Log.d(TAG, "Recording stopped") + hideRecordingIndicator() + } + + // Other callbacks... + override fun onAudioMuted() {} + override fun onAudioUnMuted() {} + override fun onVideoPaused() {} + override fun onVideoResumed() {} + override fun onScreenShareStarted() {} + override fun onScreenShareStopped() {} + override fun onAudioModeChanged(audioMode: AudioMode) {} + override fun onCameraFacingChanged(facing: CameraFacing) {} +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addMediaEventsListener(this, new MediaEventsListener() { + @Override + public void onRecordingStarted() { + Log.d(TAG, "Recording started"); + showRecordingIndicator(); + } + + @Override + public void onRecordingStopped() { + Log.d(TAG, "Recording stopped"); + hideRecordingIndicator(); + } + + // Other callbacks... + @Override public void onAudioMuted() {} + @Override public void onAudioUnMuted() {} + @Override public void onVideoPaused() {} + @Override public void onVideoResumed() {} + @Override public void onScreenShareStarted() {} + @Override public void onScreenShareStopped() {} + @Override public void onAudioModeChanged(AudioMode audioMode) {} + @Override public void onCameraFacingChanged(CameraFacing facing) {} +}); +``` + + + +## Track Participant Recording + +Monitor when other participants start or stop recording using `ParticipantEventListener`: + + + +```kotlin +callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantStartedRecording(participant: Participant) { + Log.d(TAG, "${participant.name} started recording") + } + + override fun onParticipantStoppedRecording(participant: Participant) { + Log.d(TAG, "${participant.name} stopped recording") + } + + // Other callbacks... +}) +``` + + +```java +callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantStartedRecording(Participant participant) { + Log.d(TAG, participant.getName() + " started recording"); + } + + @Override + public void onParticipantStoppedRecording(Participant participant) { + Log.d(TAG, participant.getName() + " stopped recording"); + } + + // Other callbacks... +}); +``` + + + +## Access Recordings + +Recordings are available after the call ends. You can access them in two ways: + +1. **CometChat Dashboard**: Navigate to **Calls > Call Logs** in your [CometChat Dashboard](https://app.cometchat.com) to view and download recordings. + +2. **Programmatically**: Fetch recordings through [Call Logs](/calls/android/call-logs): + + + +```kotlin +val callLogRequest = CallLogRequest.CallLogRequestBuilder() + .setHasRecording(true) + .build() + +callLogRequest.fetchNext(object : CometChatCalls.CallbackListener>() { + override fun onSuccess(callLogs: List) { + for (callLog in callLogs) { + callLog.recordings?.forEach { recording -> + Log.d(TAG, "Recording URL: ${recording.recordingURL}") + Log.d(TAG, "Duration: ${recording.duration} seconds") + Log.d(TAG, "Start Time: ${recording.startTime}") + Log.d(TAG, "End Time: ${recording.endTime}") + } + } + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Error: ${e.message}") + } +}) +``` + + +```java +CallLogRequest callLogRequest = new CallLogRequest.CallLogRequestBuilder() + .setHasRecording(true) + .build(); + +callLogRequest.fetchNext(new CometChatCalls.CallbackListener>() { + @Override + public void onSuccess(List callLogs) { + for (CallLog callLog : callLogs) { + for (Recording recording : callLog.getRecordings()) { + Log.d(TAG, "Recording URL: " + recording.getRecordingURL()); + Log.d(TAG, "Duration: " + recording.getDuration() + " seconds"); + Log.d(TAG, "Start Time: " + recording.getStartTime()); + Log.d(TAG, "End Time: " + recording.getEndTime()); + } + } + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Error: " + e.getMessage()); + } +}); +``` + + + +## Recording Object + +| Property | Type | Description | +|----------|------|-------------| +| `rid` | String | Unique recording identifier | +| `recordingURL` | String | URL to download/stream the recording | +| `startTime` | int | Timestamp when recording started | +| `endTime` | int | Timestamp when recording ended | +| `duration` | double | Recording duration in seconds | diff --git a/calls/android/ringing.mdx b/calls/android/ringing.mdx new file mode 100644 index 00000000..04c2534c --- /dev/null +++ b/calls/android/ringing.mdx @@ -0,0 +1,504 @@ +--- +title: "Ringing" +sidebarTitle: "Ringing" +--- + +Implement incoming and outgoing call notifications with accept/reject functionality. Ringing enables real-time call signaling between users, allowing them to initiate calls and respond to incoming call requests. + + +Ringing functionality requires the CometChat Chat SDK to be integrated alongside the Calls SDK. The Chat SDK handles call signaling (initiating, accepting, rejecting calls), while the Calls SDK manages the actual call session. + + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- CometChat Chat SDK initialized and user logged in +- User [logged in](/calls/android/authentication) to Calls SDK + +## How Ringing Works + +The ringing flow involves two SDKs working together: + +1. **Chat SDK** - Handles call signaling (initiate, accept, reject, cancel) +2. **Calls SDK** - Manages the actual call session once accepted + +```mermaid +sequenceDiagram + participant Caller + participant ChatSDK + participant Receiver + participant CallsSDK + + Caller->>ChatSDK: initiateCall() + ChatSDK->>Receiver: onIncomingCallReceived + Receiver->>ChatSDK: acceptCall() + ChatSDK-->>Caller: onOutgoingCallAccepted + Caller->>CallsSDK: joinSession() + Receiver->>CallsSDK: joinSession() +``` + +## Initiate a Call + +Use the Chat SDK to initiate a call to a user or group: + + + +```kotlin +val receiverID = "USER_ID" +val receiverType = CometChatConstants.RECEIVER_TYPE_USER +val callType = CometChatConstants.CALL_TYPE_VIDEO + +val call = Call(receiverID, receiverType, callType) + +CometChat.initiateCall(call, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "Call initiated: ${call.sessionId}") + // Show outgoing call UI + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Call initiation failed: ${e.message}") + } +}) +``` + + +```java +String receiverID = "USER_ID"; +String receiverType = CometChatConstants.RECEIVER_TYPE_USER; +String callType = CometChatConstants.CALL_TYPE_VIDEO; + +Call call = new Call(receiverID, receiverType, callType); + +CometChat.initiateCall(call, new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "Call initiated: " + call.getSessionId()); + // Show outgoing call UI + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Call initiation failed: " + e.getMessage()); + } +}); +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `receiverID` | String | UID of the user or GUID of the group to call | +| `receiverType` | String | `CometChatConstants.RECEIVER_TYPE_USER` or `RECEIVER_TYPE_GROUP` | +| `callType` | String | `CometChatConstants.CALL_TYPE_VIDEO` or `CALL_TYPE_AUDIO` | + +## Listen for Incoming Calls + +Register a call listener to receive incoming call notifications: + + + +```kotlin +val listenerID = "UNIQUE_LISTENER_ID" + +CometChat.addCallListener(listenerID, object : CometChat.CallListener() { + override fun onIncomingCallReceived(call: Call) { + Log.d(TAG, "Incoming call from: ${call.callInitiator.name}") + // Show incoming call UI with accept/reject options + } + + override fun onOutgoingCallAccepted(call: Call) { + Log.d(TAG, "Call accepted, joining session...") + joinCallSession(call.sessionId) + } + + override fun onOutgoingCallRejected(call: Call) { + Log.d(TAG, "Call rejected") + // Dismiss outgoing call UI + } + + override fun onIncomingCallCancelled(call: Call) { + Log.d(TAG, "Incoming call cancelled") + // Dismiss incoming call UI + } + + override fun onCallEndedMessageReceived(call: Call) { + Log.d(TAG, "Call ended") + } +}) +``` + + +```java +String listenerID = "UNIQUE_LISTENER_ID"; + +CometChat.addCallListener(listenerID, new CometChat.CallListener() { + @Override + public void onIncomingCallReceived(Call call) { + Log.d(TAG, "Incoming call from: " + call.getCallInitiator().getName()); + // Show incoming call UI with accept/reject options + } + + @Override + public void onOutgoingCallAccepted(Call call) { + Log.d(TAG, "Call accepted, joining session..."); + joinCallSession(call.getSessionId()); + } + + @Override + public void onOutgoingCallRejected(Call call) { + Log.d(TAG, "Call rejected"); + // Dismiss outgoing call UI + } + + @Override + public void onIncomingCallCancelled(Call call) { + Log.d(TAG, "Incoming call cancelled"); + // Dismiss incoming call UI + } + + @Override + public void onCallEndedMessageReceived(Call call) { + Log.d(TAG, "Call ended"); + } +}); +``` + + + +| Callback | Description | +|----------|-------------| +| `onIncomingCallReceived` | A new incoming call is received | +| `onOutgoingCallAccepted` | The receiver accepted your outgoing call | +| `onOutgoingCallRejected` | The receiver rejected your outgoing call | +| `onIncomingCallCancelled` | The caller cancelled the incoming call | +| `onCallEndedMessageReceived` | The call has ended | + + +Remember to remove the call listener when it's no longer needed to prevent memory leaks: +```kotlin +CometChat.removeCallListener(listenerID) +``` + + +## Accept a Call + +When an incoming call is received, accept it using the Chat SDK: + + + +```kotlin +fun acceptIncomingCall(sessionId: String) { + CometChat.acceptCall(sessionId, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "Call accepted") + joinCallSession(call.sessionId) + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Accept call failed: ${e.message}") + } + }) +} +``` + + +```java +void acceptIncomingCall(String sessionId) { + CometChat.acceptCall(sessionId, new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "Call accepted"); + joinCallSession(call.getSessionId()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Accept call failed: " + e.getMessage()); + } + }); +} +``` + + + +## Reject a Call + +Reject an incoming call: + + + +```kotlin +fun rejectIncomingCall(sessionId: String) { + val status = CometChatConstants.CALL_STATUS_REJECTED + + CometChat.rejectCall(sessionId, status, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "Call rejected") + // Dismiss incoming call UI + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Reject call failed: ${e.message}") + } + }) +} +``` + + +```java +void rejectIncomingCall(String sessionId) { + String status = CometChatConstants.CALL_STATUS_REJECTED; + + CometChat.rejectCall(sessionId, status, new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "Call rejected"); + // Dismiss incoming call UI + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Reject call failed: " + e.getMessage()); + } + }); +} +``` + + + +## Cancel a Call + +Cancel an outgoing call before it's answered: + + + +```kotlin +fun cancelOutgoingCall(sessionId: String) { + val status = CometChatConstants.CALL_STATUS_CANCELLED + + CometChat.rejectCall(sessionId, status, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "Call cancelled") + // Dismiss outgoing call UI + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Cancel call failed: ${e.message}") + } + }) +} +``` + + +```java +void cancelOutgoingCall(String sessionId) { + String status = CometChatConstants.CALL_STATUS_CANCELLED; + + CometChat.rejectCall(sessionId, status, new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "Call cancelled"); + // Dismiss outgoing call UI + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Cancel call failed: " + e.getMessage()); + } + }); +} +``` + + + +## Join the Call Session + +After accepting a call (or when your outgoing call is accepted), join the call session using the Calls SDK: + + + +```kotlin +fun joinCallSession(sessionId: String) { + val callViewContainer = findViewById(R.id.call_view_container) + + val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setType(SessionType.VIDEO) + .build() + + CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + object : CometChatCalls.CallbackListener() { + override fun onSuccess(callSession: CallSession) { + Log.d(TAG, "Joined call session") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed to join: ${e.message}") + } + } + ) +} +``` + + +```java +void joinCallSession(String sessionId) { + RelativeLayout callViewContainer = findViewById(R.id.call_view_container); + + SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setType(SessionType.VIDEO) + .build(); + + CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession callSession) { + Log.d(TAG, "Joined call session"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to join: " + e.getMessage()); + } + } + ); +} +``` + + + + +For more details on session customization and controlling the call, see [Actions](/calls/android/actions) and [Listeners](/calls/android/listeners). + + +## End a Call + +Properly ending a call requires coordination between both SDKs to ensure all participants are notified and call logs are recorded correctly. + + +Always call `CometChat.endCall()` when ending a call. This notifies the other participant and ensures the call is properly logged. Without this, the other user won't know the call has ended and call logs may be incomplete. + + +```mermaid +sequenceDiagram + participant User + participant CallsSDK + participant ChatSDK + participant OtherParticipant + + User->>CallsSDK: leaveSession() + User->>ChatSDK: endCall(sessionId) + ChatSDK->>OtherParticipant: onCallEndedMessageReceived + OtherParticipant->>CallsSDK: leaveSession() +``` + +When using the default call UI, listen for the end call button click using `ButtonClickListener` and call `endCall()`: + + + +```kotlin +val callSession = CallSession.getInstance() + +// Listen for end call button click +callSession.addButtonClickListener(this, object : ButtonClickListener() { + override fun onLeaveSessionButtonClicked() { + endCall(currentSessionId) + } + // Other callbacks... +}) + +fun endCall(sessionId: String) { + // 1. Leave the call session (Calls SDK) + CallSession.getInstance().leaveSession() + + // 2. Notify other participants (Chat SDK) + CometChat.endCall(sessionId, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "Call ended successfully") + finish() + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "End call failed: ${e.message}") + finish() + } + }) +} +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +// Listen for end call button click +callSession.addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onLeaveSessionButtonClicked() { + endCall(currentSessionId); + } + // Other callbacks... +}); + +void endCall(String sessionId) { + // 1. Leave the call session (Calls SDK) + CallSession.getInstance().leaveSession(); + + // 2. Notify other participants (Chat SDK) + CometChat.endCall(sessionId, new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "Call ended successfully"); + finish(); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "End call failed: " + e.getMessage()); + finish(); + } + }); +} +``` + + + +The other participant receives `onCallEndedMessageReceived` callback and should leave the session: + + + +```kotlin +CometChat.addCallListener(listenerID, object : CometChat.CallListener() { + override fun onCallEndedMessageReceived(call: Call) { + CallSession.getInstance().leaveSession() + finish() + } + // Other callbacks... +}) +``` + + +```java +CometChat.addCallListener(listenerID, new CometChat.CallListener() { + @Override + public void onCallEndedMessageReceived(Call call) { + CallSession.getInstance().leaveSession(); + finish(); + } + // Other callbacks... +}); +``` + + + +## Call Status Values + +| Status | Description | +|--------|-------------| +| `initiated` | Call has been initiated but not yet answered | +| `ongoing` | Call is currently in progress | +| `busy` | Receiver is busy on another call | +| `rejected` | Receiver rejected the call | +| `cancelled` | Caller cancelled before receiver answered | +| `ended` | Call ended normally | +| `missed` | Receiver didn't answer in time | +| `unanswered` | Call was not answered | diff --git a/calls/android/screen-sharing.mdx b/calls/android/screen-sharing.mdx new file mode 100644 index 00000000..b9c4b125 --- /dev/null +++ b/calls/android/screen-sharing.mdx @@ -0,0 +1,116 @@ +--- +title: "Screen Sharing" +sidebarTitle: "Screen Sharing" +--- + +View screen shares from other participants during a call. The Android SDK can receive and display screen shares initiated from web clients. + + +The Android Calls SDK does not support initiating screen sharing. Screen sharing can only be started from web clients. Android participants can view shared screens. + + +## Prerequisites + +- CometChat Calls SDK [initialized](/calls/android/setup) +- User [logged in](/calls/android/authentication) +- An active [call session](/calls/android/join-session) + +## How It Works + +When a web participant starts screen sharing: +1. The SDK receives the screen share stream +2. The call layout automatically adjusts to display the shared screen prominently +3. Android participants can view the shared content + +## Listen for Screen Share Events + +Monitor when participants start or stop screen sharing: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantStartedScreenShare(participant: Participant) { + Log.d(TAG, "${participant.name} started screen sharing") + // Layout automatically adjusts to show shared screen + } + + override fun onParticipantStoppedScreenShare(participant: Participant) { + Log.d(TAG, "${participant.name} stopped screen sharing") + // Layout returns to normal view + } + + // Other callbacks... + override fun onParticipantJoined(participant: Participant) {} + override fun onParticipantLeft(participant: Participant) {} + override fun onParticipantListChanged(participants: List) {} + override fun onParticipantAudioMuted(participant: Participant) {} + override fun onParticipantAudioUnmuted(participant: Participant) {} + override fun onParticipantVideoPaused(participant: Participant) {} + override fun onParticipantVideoResumed(participant: Participant) {} + override fun onParticipantStartedRecording(participant: Participant) {} + override fun onParticipantStoppedRecording(participant: Participant) {} + override fun onParticipantHandRaised(participant: Participant) {} + override fun onParticipantHandLowered(participant: Participant) {} + override fun onDominantSpeakerChanged(participant: Participant) {} +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantStartedScreenShare(Participant participant) { + Log.d(TAG, participant.getName() + " started screen sharing"); + // Layout automatically adjusts to show shared screen + } + + @Override + public void onParticipantStoppedScreenShare(Participant participant) { + Log.d(TAG, participant.getName() + " stopped screen sharing"); + // Layout returns to normal view + } + + // Other callbacks... +}); +``` + + + +## Check Screen Share Status + +Use the `isPresenting` property on the `Participant` object to check if someone is sharing their screen: + + + +```kotlin +callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantListChanged(participants: List) { + val presenter = participants.find { it.isPresenting } + if (presenter != null) { + Log.d(TAG, "${presenter.name} is currently sharing their screen") + } + } +}) +``` + + +```java +callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantListChanged(List participants) { + for (Participant p : participants) { + if (p.isPresenting()) { + Log.d(TAG, p.getName() + " is currently sharing their screen"); + break; + } + } + } +}); +``` + + diff --git a/calls/android/session-control.mdx b/calls/android/session-control.mdx new file mode 100644 index 00000000..38d91d9f --- /dev/null +++ b/calls/android/session-control.mdx @@ -0,0 +1,437 @@ +--- +title: "Session Control" +sidebarTitle: "Session Control" +--- + +Control the call session lifecycle and participant interactions. These methods allow you to leave the session, raise/lower your hand, and check session status. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance + +## Get CallSession Instance + +Session control methods are called on the `CallSession` singleton: + + + +```kotlin +val callSession = CallSession.getInstance() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +``` + + + +--- + +## Check Session Status + +Check if there is currently an active call session. + + + +```kotlin +val isActive = callSession.isSessionActive() + +if (isActive) { + Log.d(TAG, "Call session is active") +} else { + Log.d(TAG, "No active call session") +} +``` + + +```java +boolean isActive = callSession.isSessionActive(); + +if (isActive) { + Log.d(TAG, "Call session is active"); +} else { + Log.d(TAG, "No active call session"); +} +``` + + + +| Return Type | Description | +|-------------|-------------| +| `Boolean` | `true` if a session is active, `false` otherwise | + + +Use this method to check session status before calling other `CallSession` methods, or to determine if you need to show call UI. + + +--- + +## Leave Session + +End your participation in the current call session. + + + +```kotlin +callSession.leaveSession() +``` + + +```java +callSession.leaveSession(); +``` + + + + +When you leave the session, the `onSessionLeft()` callback is triggered on your `SessionStatusListener`. Other participants will receive the `onParticipantLeft(Participant)` callback. + + +### Handling Session End + + + +```kotlin +callSession.addSessionStatusListener(this, object : SessionStatusListener { + override fun onSessionLeft() { + Log.d(TAG, "Successfully left the session") + // Navigate away from call screen + finish() + } + + override fun onSessionJoined() {} + override fun onSessionTimedOut() {} + override fun onConnectionLost() {} + override fun onConnectionRestored() {} + override fun onConnectionClosed() {} +}) +``` + + +```java +callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionLeft() { + Log.d(TAG, "Successfully left the session"); + // Navigate away from call screen + finish(); + } + + @Override + public void onSessionJoined() {} + @Override + public void onSessionTimedOut() {} + @Override + public void onConnectionLost() {} + @Override + public void onConnectionRestored() {} + @Override + public void onConnectionClosed() {} +}); +``` + + + +--- + +## Raise Hand + +Raise your hand to get attention from other participants. This is useful in meetings or webinars when you want to ask a question or make a comment. + + + +```kotlin +callSession.raiseHand() +``` + + +```java +callSession.raiseHand(); +``` + + + + +When you raise your hand, all participants receive the `onParticipantHandRaised(Participant)` callback on their `ParticipantEventListener`. + + +--- + +## Lower Hand + +Lower your previously raised hand. + + + +```kotlin +callSession.lowerHand() +``` + + +```java +callSession.lowerHand(); +``` + + + + +When you lower your hand, all participants receive the `onParticipantHandLowered(Participant)` callback on their `ParticipantEventListener`. + + +--- + +## Listen for Hand Raise Events + +Register a `ParticipantEventListener` to receive callbacks when participants raise or lower their hands: + + + +```kotlin +callSession.addParticipantEventListener(this, object : ParticipantEventListener { + override fun onParticipantHandRaised(participant: Participant) { + Log.d(TAG, "${participant.name} raised their hand") + // Show hand raised indicator in UI + } + + override fun onParticipantHandLowered(participant: Participant) { + Log.d(TAG, "${participant.name} lowered their hand") + // Hide hand raised indicator in UI + } + + // Other callbacks... + override fun onParticipantJoined(participant: Participant) {} + override fun onParticipantLeft(participant: Participant) {} + override fun onParticipantAudioMuted(participant: Participant) {} + override fun onParticipantAudioUnmuted(participant: Participant) {} + override fun onParticipantVideoPaused(participant: Participant) {} + override fun onParticipantVideoResumed(participant: Participant) {} + override fun onParticipantStartedScreenShare(participant: Participant) {} + override fun onParticipantStoppedScreenShare(participant: Participant) {} + override fun onParticipantStartedRecording(participant: Participant) {} + override fun onParticipantStoppedRecording(participant: Participant) {} + override fun onDominantSpeakerChanged(participant: Participant) {} + override fun onParticipantListChanged(participants: List) {} +}) +``` + + +```java +callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantHandRaised(Participant participant) { + Log.d(TAG, participant.getName() + " raised their hand"); + // Show hand raised indicator in UI + } + + @Override + public void onParticipantHandLowered(Participant participant) { + Log.d(TAG, participant.getName() + " lowered their hand"); + // Hide hand raised indicator in UI + } + + // Other callbacks... + @Override + public void onParticipantJoined(Participant participant) {} + @Override + public void onParticipantLeft(Participant participant) {} + @Override + public void onParticipantAudioMuted(Participant participant) {} + @Override + public void onParticipantAudioUnmuted(Participant participant) {} + @Override + public void onParticipantVideoPaused(Participant participant) {} + @Override + public void onParticipantVideoResumed(Participant participant) {} + @Override + public void onParticipantStartedScreenShare(Participant participant) {} + @Override + public void onParticipantStoppedScreenShare(Participant participant) {} + @Override + public void onParticipantStartedRecording(Participant participant) {} + @Override + public void onParticipantStoppedRecording(Participant participant) {} + @Override + public void onDominantSpeakerChanged(Participant participant) {} + @Override + public void onParticipantListChanged(List participants) {} +}); +``` + + + +--- + +## Button Click Listeners + +Listen for when users tap the leave or raise hand buttons: + + + +```kotlin +callSession.addButtonClickListener(this, object : ButtonClickListener { + override fun onLeaveSessionButtonClicked() { + Log.d(TAG, "Leave button clicked") + // Optionally show confirmation dialog before leaving + } + + override fun onRaiseHandButtonClicked() { + Log.d(TAG, "Raise hand button clicked") + // Handle custom raise hand logic if needed + } + + // Other callbacks... + override fun onShareInviteButtonClicked() {} + override fun onChangeLayoutButtonClicked() {} + override fun onParticipantListButtonClicked() {} + override fun onToggleAudioButtonClicked() {} + override fun onToggleVideoButtonClicked() {} + override fun onSwitchCameraButtonClicked() {} + override fun onChatButtonClicked() {} + override fun onRecordingToggleButtonClicked() {} +}) +``` + + +```java +callSession.addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onLeaveSessionButtonClicked() { + Log.d(TAG, "Leave button clicked"); + // Optionally show confirmation dialog before leaving + } + + @Override + public void onRaiseHandButtonClicked() { + Log.d(TAG, "Raise hand button clicked"); + // Handle custom raise hand logic if needed + } + + // Other callbacks... + @Override + public void onShareInviteButtonClicked() {} + @Override + public void onChangeLayoutButtonClicked() {} + @Override + public void onParticipantListButtonClicked() {} + @Override + public void onToggleAudioButtonClicked() {} + @Override + public void onToggleVideoButtonClicked() {} + @Override + public void onSwitchCameraButtonClicked() {} + @Override + public void onChatButtonClicked() {} + @Override + public void onRecordingToggleButtonClicked() {} +}); +``` + + + +--- + +## Hide Session Control Buttons + +Control the visibility of session control buttons in the UI: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideLeaveSessionButton(false) // Show leave button + .hideRaiseHandButton(false) // Show raise hand button + .hideShareInviteButton(true) // Hide share invite button + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideLeaveSessionButton(false) // Show leave button + .hideRaiseHandButton(false) // Show raise hand button + .hideShareInviteButton(true) // Hide share invite button + .build(); +``` + + + +--- + +## Session Timeout + +Configure the idle timeout period for when you're alone in a session: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setIdleTimeoutPeriod(300) // 300 seconds (5 minutes) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setIdleTimeoutPeriod(300) // 300 seconds (5 minutes) + .build(); +``` + + + +When the timeout is reached, the `onSessionTimedOut()` callback is triggered: + + + +```kotlin +callSession.addSessionStatusListener(this, object : SessionStatusListener { + override fun onSessionTimedOut() { + Log.d(TAG, "Session timed out due to inactivity") + // Handle timeout - navigate away from call screen + finish() + } + + override fun onSessionJoined() {} + override fun onSessionLeft() {} + override fun onConnectionLost() {} + override fun onConnectionRestored() {} + override fun onConnectionClosed() {} +}) +``` + + +```java +callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionTimedOut() { + Log.d(TAG, "Session timed out due to inactivity"); + // Handle timeout - navigate away from call screen + finish(); + } + + @Override + public void onSessionJoined() {} + @Override + public void onSessionLeft() {} + @Override + public void onConnectionLost() {} + @Override + public void onConnectionRestored() {} + @Override + public void onConnectionClosed() {} +}); +``` + + + +## Next Steps + + + + Handle all session lifecycle events + + + Handle all button click events + + diff --git a/calls/android/session-status-listener.mdx b/calls/android/session-status-listener.mdx new file mode 100644 index 00000000..71070673 --- /dev/null +++ b/calls/android/session-status-listener.mdx @@ -0,0 +1,503 @@ +--- +title: "Session Status Listener" +sidebarTitle: "Session Status Listener" +--- + +Monitor the call session lifecycle with `SessionStatusListener`. This listener provides callbacks for session join/leave events, connection status changes, and session timeouts. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance + +## Register Listener + +Register a `SessionStatusListener` to receive session status callbacks: + + + +```kotlin +val callSession = CallSession.getInstance() + +callSession.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionJoined() { + Log.d(TAG, "Successfully joined the session") + } + + override fun onSessionLeft() { + Log.d(TAG, "Left the session") + } + + override fun onSessionTimedOut() { + Log.d(TAG, "Session timed out") + } + + override fun onConnectionLost() { + Log.d(TAG, "Connection lost") + } + + override fun onConnectionRestored() { + Log.d(TAG, "Connection restored") + } + + override fun onConnectionClosed() { + Log.d(TAG, "Connection closed") + } +}) +``` + + +```java +CallSession callSession = CallSession.getInstance(); + +callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionJoined() { + Log.d(TAG, "Successfully joined the session"); + } + + @Override + public void onSessionLeft() { + Log.d(TAG, "Left the session"); + } + + @Override + public void onSessionTimedOut() { + Log.d(TAG, "Session timed out"); + } + + @Override + public void onConnectionLost() { + Log.d(TAG, "Connection lost"); + } + + @Override + public void onConnectionRestored() { + Log.d(TAG, "Connection restored"); + } + + @Override + public void onConnectionClosed() { + Log.d(TAG, "Connection closed"); + } +}); +``` + + + + +The listener is automatically removed when the `LifecycleOwner` (Activity/Fragment) is destroyed, preventing memory leaks. + + +--- + +## Callbacks + +### onSessionJoined + +Triggered when you successfully join a call session. + + + +```kotlin +override fun onSessionJoined() { + Log.d(TAG, "Successfully joined the session") + // Update UI to show call screen + // Start any call-related services +} +``` + + +```java +@Override +public void onSessionJoined() { + Log.d(TAG, "Successfully joined the session"); + // Update UI to show call screen + // Start any call-related services +} +``` + + + +**Use Cases:** +- Update UI to display the call interface +- Start foreground service for ongoing call notification +- Initialize call-related features + +--- + +### onSessionLeft + +Triggered when you leave the call session (either by calling `leaveSession()` or being removed). + + + +```kotlin +override fun onSessionLeft() { + Log.d(TAG, "Left the session") + // Clean up resources + // Navigate back to previous screen + finish() +} +``` + + +```java +@Override +public void onSessionLeft() { + Log.d(TAG, "Left the session"); + // Clean up resources + // Navigate back to previous screen + finish(); +} +``` + + + +**Use Cases:** +- Clean up call-related resources +- Stop foreground service +- Navigate away from call screen + +--- + +### onSessionTimedOut + +Triggered when the session times out due to inactivity (e.g., being alone in the call for too long). + + + +```kotlin +override fun onSessionTimedOut() { + Log.d(TAG, "Session timed out due to inactivity") + // Show timeout message to user + Toast.makeText(this, "Call ended due to inactivity", Toast.LENGTH_SHORT).show() + finish() +} +``` + + +```java +@Override +public void onSessionTimedOut() { + Log.d(TAG, "Session timed out due to inactivity"); + // Show timeout message to user + Toast.makeText(this, "Call ended due to inactivity", Toast.LENGTH_SHORT).show(); + finish(); +} +``` + + + + +Configure the timeout period using `setIdleTimeoutPeriod()` in [SessionSettings](/calls/android/session-settings). Default is 300 seconds (5 minutes). + + +**Use Cases:** +- Display timeout notification to user +- Clean up resources and navigate away +- Log analytics event + +--- + +### onConnectionLost + +Triggered when the connection to the call server is lost (e.g., network issues). + + + +```kotlin +override fun onConnectionLost() { + Log.d(TAG, "Connection lost - attempting to reconnect") + // Show reconnecting indicator + showReconnectingUI() +} +``` + + +```java +@Override +public void onConnectionLost() { + Log.d(TAG, "Connection lost - attempting to reconnect"); + // Show reconnecting indicator + showReconnectingUI(); +} +``` + + + +**Use Cases:** +- Display "Reconnecting..." indicator +- Disable call controls temporarily +- Log connection issue for debugging + +--- + +### onConnectionRestored + +Triggered when the connection is restored after being lost. + + + +```kotlin +override fun onConnectionRestored() { + Log.d(TAG, "Connection restored") + // Hide reconnecting indicator + hideReconnectingUI() + // Re-enable call controls +} +``` + + +```java +@Override +public void onConnectionRestored() { + Log.d(TAG, "Connection restored"); + // Hide reconnecting indicator + hideReconnectingUI(); + // Re-enable call controls +} +``` + + + +**Use Cases:** +- Hide reconnecting indicator +- Re-enable call controls +- Show success notification + +--- + +### onConnectionClosed + +Triggered when the connection is permanently closed (cannot be restored). + + + +```kotlin +override fun onConnectionClosed() { + Log.d(TAG, "Connection closed permanently") + // Show error message + Toast.makeText(this, "Call connection lost", Toast.LENGTH_SHORT).show() + // Clean up and exit + finish() +} +``` + + +```java +@Override +public void onConnectionClosed() { + Log.d(TAG, "Connection closed permanently"); + // Show error message + Toast.makeText(this, "Call connection lost", Toast.LENGTH_SHORT).show(); + // Clean up and exit + finish(); +} +``` + + + +**Use Cases:** +- Display error message to user +- Clean up resources +- Navigate away from call screen + +--- + +## Complete Example + +Here's a complete example handling all session status events: + + + +```kotlin +class CallActivity : AppCompatActivity() { + private lateinit var callSession: CallSession + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + callSession = CallSession.getInstance() + setupSessionStatusListener() + } + + private fun setupSessionStatusListener() { + callSession.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionJoined() { + Log.d(TAG, "Session joined") + runOnUiThread { + // Start ongoing call service + CometChatOngoingCallService.launch(this@CallActivity) + } + } + + override fun onSessionLeft() { + Log.d(TAG, "Session left") + runOnUiThread { + CometChatOngoingCallService.abort(this@CallActivity) + finish() + } + } + + override fun onSessionTimedOut() { + Log.d(TAG, "Session timed out") + runOnUiThread { + Toast.makeText( + this@CallActivity, + "Call ended due to inactivity", + Toast.LENGTH_SHORT + ).show() + CometChatOngoingCallService.abort(this@CallActivity) + finish() + } + } + + override fun onConnectionLost() { + Log.d(TAG, "Connection lost") + runOnUiThread { + showReconnectingOverlay(true) + } + } + + override fun onConnectionRestored() { + Log.d(TAG, "Connection restored") + runOnUiThread { + showReconnectingOverlay(false) + } + } + + override fun onConnectionClosed() { + Log.d(TAG, "Connection closed") + runOnUiThread { + Toast.makeText( + this@CallActivity, + "Connection lost. Please try again.", + Toast.LENGTH_SHORT + ).show() + CometChatOngoingCallService.abort(this@CallActivity) + finish() + } + } + }) + } + + private fun showReconnectingOverlay(show: Boolean) { + // Show/hide reconnecting UI overlay + } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + private static final String TAG = "CallActivity"; + private CallSession callSession; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + callSession = CallSession.getInstance(); + setupSessionStatusListener(); + } + + private void setupSessionStatusListener() { + callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionJoined() { + Log.d(TAG, "Session joined"); + runOnUiThread(() -> { + // Start ongoing call service + CometChatOngoingCallService.launch(CallActivity.this); + }); + } + + @Override + public void onSessionLeft() { + Log.d(TAG, "Session left"); + runOnUiThread(() -> { + CometChatOngoingCallService.abort(CallActivity.this); + finish(); + }); + } + + @Override + public void onSessionTimedOut() { + Log.d(TAG, "Session timed out"); + runOnUiThread(() -> { + Toast.makeText( + CallActivity.this, + "Call ended due to inactivity", + Toast.LENGTH_SHORT + ).show(); + CometChatOngoingCallService.abort(CallActivity.this); + finish(); + }); + } + + @Override + public void onConnectionLost() { + Log.d(TAG, "Connection lost"); + runOnUiThread(() -> showReconnectingOverlay(true)); + } + + @Override + public void onConnectionRestored() { + Log.d(TAG, "Connection restored"); + runOnUiThread(() -> showReconnectingOverlay(false)); + } + + @Override + public void onConnectionClosed() { + Log.d(TAG, "Connection closed"); + runOnUiThread(() -> { + Toast.makeText( + CallActivity.this, + "Connection lost. Please try again.", + Toast.LENGTH_SHORT + ).show(); + CometChatOngoingCallService.abort(CallActivity.this); + finish(); + }); + } + }); + } + + private void showReconnectingOverlay(boolean show) { + // Show/hide reconnecting UI overlay + } +} +``` + + + +--- + +## Callbacks Summary + +| Callback | Description | +|----------|-------------| +| `onSessionJoined()` | Successfully joined the call session | +| `onSessionLeft()` | Left the call session | +| `onSessionTimedOut()` | Session ended due to inactivity timeout | +| `onConnectionLost()` | Connection to server lost (reconnecting) | +| `onConnectionRestored()` | Connection restored after being lost | +| `onConnectionClosed()` | Connection permanently closed | + +## Next Steps + + + + Handle participant join/leave and state changes + + + Handle recording, screen share, and media state changes + + diff --git a/calls/android/setup.mdx b/calls/android/setup.mdx new file mode 100644 index 00000000..da122a55 --- /dev/null +++ b/calls/android/setup.mdx @@ -0,0 +1,203 @@ +--- +title: "Setup" +sidebarTitle: "Setup" +--- + +This guide walks you through installing the CometChat Calls SDK and initializing it in your Android application. + +## Prerequisites + +Before you begin, ensure you have: + +1. **CometChat Account**: [Sign up](https://app.cometchat.com/signup) and create an app +2. **Credentials**: Note your **App ID**, **Region**, and **API Key** from the CometChat Dashboard +3. **Android Requirements**: + - Minimum SDK: API Level 24 (Android 7.0) + - AndroidX compatibility + - Java 8 or higher + +## Add the CometChat Dependency + +### Step 1: Add Repository + +Add the CometChat repository URL to your **project level** `build.gradle` file in the `repositories` block: + + + +```groovy +allprojects { + repositories { + maven { + url "https://dl.cloudsmith.io/public/cometchat/cometchat/maven/" + } + } +} +``` + + +```kotlin +allprojects { + repositories { + maven { + url = uri("https://dl.cloudsmith.io/public/cometchat/cometchat/maven/") + } + } +} +``` + + + +### Step 2: Add Dependencies + +Add the Calls SDK dependency to your **app level** `build.gradle` file: + + + +```groovy +dependencies { + implementation "com.cometchat:calls-sdk-android:5.0.0" +} +``` + + +```kotlin +dependencies { + implementation("com.cometchat:calls-sdk-android:5.0.0") +} +``` + + + +### Step 3: Configure Java Version + +Add Java 8 compatibility to the `android` section of your **app level** `build.gradle`: + + + +```groovy +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} +``` + + +```kotlin +android { + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } +} +``` + + + +## Add Permissions + +Add the required permissions to your `AndroidManifest.xml`: + +```xml + + + + + +``` + + +For Android 6.0 (API level 23) and above, you must request camera and microphone permissions at runtime before starting a call. + + +## Initialize CometChat Calls + +The `init()` method initializes the SDK with your app credentials. Call this method once when your application starts, typically in your `Application` class or main `Activity`. + +### CallAppSettings + +The `CallAppSettings` class configures the SDK initialization: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `appId` | String | Yes | Your CometChat App ID | +| `region` | String | Yes | Your app region (`us` or `eu`) | + + + +```kotlin +import com.cometchat.calls.core.CometChatCalls +import com.cometchat.calls.core.CallAppSettings + +val appId = "APP_ID" // Replace with your App ID +val region = "REGION" // Replace with your Region ("us" or "eu") + +val callAppSettings = CallAppSettings.CallAppSettingBuilder(appId, region).build() + +CometChatCalls.init(this, callAppSettings, object : CometChatCalls.CallbackListener() { + override fun onSuccess(message: String) { + Log.d(TAG, "CometChat Calls SDK initialized successfully") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "CometChat Calls SDK initialization failed: ${e.message}") + } +}) +``` + + +```java +import com.cometchat.calls.core.CometChatCalls; +import com.cometchat.calls.core.CallAppSettings; + +String appId = "APP_ID"; // Replace with your App ID +String region = "REGION"; // Replace with your Region ("us" or "eu") + +CallAppSettings callAppSettings = new CallAppSettings.CallAppSettingBuilder(appId, region).build(); + +CometChatCalls.init(this, callAppSettings, new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(String message) { + Log.d(TAG, "CometChat Calls SDK initialized successfully"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "CometChat Calls SDK initialization failed: " + e.getMessage()); + } +}); +``` + + + +| Parameter | Description | +|-----------|-------------| +| `this` | Android context (Application or Activity context) | +| `callAppSettings` | Configuration object with App ID and Region | +| `listener` | Callback listener for success/error handling | + +## Check Initialization Status + +You can verify if the SDK has been initialized using the `isInitialized()` method: + + + +```kotlin +if (CometChatCalls.isInitialized()) { + // SDK is ready to use +} else { + // Initialize the SDK first +} +``` + + +```java +if (CometChatCalls.isInitialized()) { + // SDK is ready to use +} else { + // Initialize the SDK first +} +``` + + diff --git a/calls/android/video-controls.mdx b/calls/android/video-controls.mdx new file mode 100644 index 00000000..d512afe3 --- /dev/null +++ b/calls/android/video-controls.mdx @@ -0,0 +1,245 @@ +--- +title: "Video Controls" +sidebarTitle: "Video Controls" +--- + +Control video during an active call session. These methods allow you to pause/resume the local camera and switch between front and back cameras. + +## Prerequisites + +- An active [call session](/calls/android/join-session) +- Access to the `CallSession` instance +- Camera permissions granted + +## Get CallSession Instance + +All video control methods are called on the `CallSession` singleton: + + + +```kotlin +val callSession = CallSession.getInstance() +``` + + +```java +CallSession callSession = CallSession.getInstance(); +``` + + + +--- + +## Pause Video + +Turn off the local camera. Other participants will see a placeholder instead of your video feed. + + + +```kotlin +callSession.pauseVideo() +``` + + +```java +callSession.pauseVideo(); +``` + + + + +When you pause your video, the `onVideoPaused()` callback is triggered on your `MediaEventsListener`. + + +--- + +## Resume Video + +Turn on the local camera to resume transmitting video. + + + +```kotlin +callSession.resumeVideo() +``` + + +```java +callSession.resumeVideo(); +``` + + + + +When you resume your video, the `onVideoResumed()` callback is triggered on your `MediaEventsListener`. + + +--- + +## Switch Camera + +Toggle between the front-facing and rear cameras. + + + +```kotlin +callSession.switchCamera() +``` + + +```java +callSession.switchCamera(); +``` + + + + +When the camera is switched, the `onCameraFacingChanged(CameraFacing)` callback is triggered on your `MediaEventsListener`. + + +### CameraFacing Enum + +| Value | Description | +|-------|-------------| +| `FRONT` | Front-facing camera (selfie camera) | +| `BACK` | Rear camera | + +--- + +## Listen for Video Events + +Register a `MediaEventsListener` to receive callbacks when video state changes: + + + +```kotlin +callSession.addMediaEventsListener(this, object : MediaEventsListener { + override fun onVideoPaused() { + Log.d(TAG, "Video paused") + // Update UI to show video off state + } + + override fun onVideoResumed() { + Log.d(TAG, "Video resumed") + // Update UI to show video on state + } + + override fun onCameraFacingChanged(cameraFacing: CameraFacing) { + Log.d(TAG, "Camera switched to: ${cameraFacing.value}") + // Update UI to reflect camera change + } + + // Other MediaEventsListener callbacks... + override fun onRecordingStarted() {} + override fun onRecordingStopped() {} + override fun onScreenShareStarted() {} + override fun onScreenShareStopped() {} + override fun onAudioModeChanged(audioMode: AudioMode) {} + override fun onAudioMuted() {} + override fun onAudioUnMuted() {} +}) +``` + + +```java +callSession.addMediaEventsListener(this, new MediaEventsListener() { + @Override + public void onVideoPaused() { + Log.d(TAG, "Video paused"); + // Update UI to show video off state + } + + @Override + public void onVideoResumed() { + Log.d(TAG, "Video resumed"); + // Update UI to show video on state + } + + @Override + public void onCameraFacingChanged(CameraFacing cameraFacing) { + Log.d(TAG, "Camera switched to: " + cameraFacing.getValue()); + // Update UI to reflect camera change + } + + // Other MediaEventsListener callbacks... + @Override + public void onRecordingStarted() {} + @Override + public void onRecordingStopped() {} + @Override + public void onScreenShareStarted() {} + @Override + public void onScreenShareStopped() {} + @Override + public void onAudioModeChanged(AudioMode audioMode) {} + @Override + public void onAudioMuted() {} + @Override + public void onAudioUnMuted() {} +}); +``` + + + +--- + +## Initial Video Settings + +You can configure the initial video state when joining a session using `SessionSettings`: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .startVideoPaused(true) // Start with camera off + .setInitialCameraFacing(CameraFacing.FRONT) // Start with front camera + .setType(SessionType.VIDEO) // Video call (not audio-only) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .startVideoPaused(true) // Start with camera off + .setInitialCameraFacing(CameraFacing.FRONT) // Start with front camera + .setType(SessionType.VIDEO) // Video call (not audio-only) + .build(); +``` + + + +--- + +## Hide Video Controls in UI + +You can hide the built-in video control buttons using `SessionSettings`: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideToggleVideoButton(true) // Hide the video on/off button + .hideSwitchCameraButton(true) // Hide the camera flip button + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideToggleVideoButton(true) // Hide the video on/off button + .hideSwitchCameraButton(true) // Hide the camera flip button + .build(); +``` + + + +## Next Steps + + + + Record call sessions + + + Handle all media events + + diff --git a/calls/api/get-call.mdx b/calls/api/get-call.mdx new file mode 100644 index 00000000..89b4fbc3 --- /dev/null +++ b/calls/api/get-call.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /calls/{sessionId} +--- diff --git a/calls/api/list-calls.mdx b/calls/api/list-calls.mdx new file mode 100644 index 00000000..7e91dcd3 --- /dev/null +++ b/calls/api/list-calls.mdx @@ -0,0 +1,3 @@ +--- +openapi: get /calls +--- diff --git a/calls/api/overview.mdx b/calls/api/overview.mdx new file mode 100644 index 00000000..e32988ba --- /dev/null +++ b/calls/api/overview.mdx @@ -0,0 +1,46 @@ +--- +title: "Overview" +--- + +The Calls API provides programmatic access to the logs. Below, we have mentioned the key properties. + +| Parameters | Type | Description | +| -------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------- | +| **sessionId** | string | Specifies the call's unique ID. | +| **receiverType** | string | Specifies the receiver type of the call. Possible values: users/groups. | +| **totalAudioMinutes** | float | Indicates the total audio minutes of the call. | +| **totalVideoMinutes** | float | Indicates the total video minutes of the call. | +| **totalDurationInMinutes** | float | Indicates the total call minutes. Basically addition of audio & video minutes. | +| **totalDuration** | string | Representation of total call minutes in timer format. | +| **hasRecording** | boolean | Indicates if the call has recording in it. | +| **mode** | string | It represents the mode of the call. Possible values: call/meet/presenter | +| **startedAt** | integer | 10-digit unix timestamp indicating when the call was initiated. | +| **status** | string | It represents the current status of the call. Possible values: initiated, ongoing, ended, unanswered, rejected, canceled. | +| **type** | string | Indicating the type of the call. Possible values: audio/video | +| **totalParticipants** | integer | Indicating the participants count of the call. If a user joins from multiple devices, it would be accounted separately. | +| **endedAt** | integer | 10-digit unix timestamp indicating when the call was ended. | +| **participants** | array of objects | It includes details of individual participants who were a part of the call. | + +## Participants + +| Parameters | Type | description | +| -------------------------- | ------- | -------------------------------------------------------------------------------- | +| **uid** | string | Indicates unique identifier of an user. | +| **totalAudioMinutes** | float | Indicates the total audio minutes that the user was a part of the call. | +| **totalVideoMinutes** | float | Indicates the total video minutes that the user was a part of the call. | +| **totalDurationInMinutes** | float | Indicates the total call minutes that the user was a part of. | +| **deviceId** | string | Unique identifier of the device from where the user joined the call. | +| **isJoined** | boolean | Indicates if the user joined the call. | +| **joinedAt** | integer | 10-digit Unix timestamp indicating the joining time of the user. | +| **state** | string | Current state of the user. Possible values: ongoing, ended, unanswered, rejected | +| **leftAt** | integer | 10-digit Unix timestamp indicating the leaving time of the user from the call. | + +## Recordings + +| Parameters | Type | description | +| ------------------ | ------- | ------------------------------------------------------------------ | +| **rid** | string | Indicates unique identifier of the recording. | +| **duration** | float | Indicates total recording time of the call. | +| **startTime** | integer | 10-digit Unix timestamp indicating when the recording was started. | +| **endTime** | integer | 10-digit Unix timestamp indicating when the recording was ended. | +| **recording\_url** | string | Indicates the S3 URL of the call recording. | diff --git a/calls/flutter/overview.mdx b/calls/flutter/overview.mdx new file mode 100644 index 00000000..e37718e1 --- /dev/null +++ b/calls/flutter/overview.mdx @@ -0,0 +1,6 @@ +--- +title: "Calls SDK" +sidebarTitle: "Overview" +--- + +Documentation coming soon for Flutter Calls SDK. diff --git a/calls/ionic/overview.mdx b/calls/ionic/overview.mdx new file mode 100644 index 00000000..ab0871fd --- /dev/null +++ b/calls/ionic/overview.mdx @@ -0,0 +1,6 @@ +--- +title: "Calls SDK" +sidebarTitle: "Overview" +--- + +Documentation coming soon for Ionic Calls SDK. diff --git a/calls/ios/overview.mdx b/calls/ios/overview.mdx new file mode 100644 index 00000000..3d0f2822 --- /dev/null +++ b/calls/ios/overview.mdx @@ -0,0 +1,6 @@ +--- +title: "Calls SDK" +sidebarTitle: "Overview" +--- + +Documentation coming soon for iOS Calls SDK. diff --git a/calls/javascript/overview.mdx b/calls/javascript/overview.mdx new file mode 100644 index 00000000..084f5b29 --- /dev/null +++ b/calls/javascript/overview.mdx @@ -0,0 +1,6 @@ +--- +title: "Calls SDK" +sidebarTitle: "Overview" +--- + +Documentation coming soon for JavaScript Calls SDK. diff --git a/calls/react-native/overview.mdx b/calls/react-native/overview.mdx new file mode 100644 index 00000000..599bf5a1 --- /dev/null +++ b/calls/react-native/overview.mdx @@ -0,0 +1,6 @@ +--- +title: "Calls SDK" +sidebarTitle: "Overview" +--- + +Documentation coming soon for React Native Calls SDK. diff --git a/docs.json b/docs.json index 8f9b6b5d..5d85f63d 100644 --- a/docs.json +++ b/docs.json @@ -4813,6 +4813,114 @@ } ] }, + { + "product": "Calling", + "tabs": [ + { + "tab": "Calling", + "pages": [ + "calls" + ] + }, + { + "tab": "SDK", + "tab-id": "calls-sdk", + "dropdowns": [ + { + "dropdown": "Android", + "icon": "/images/icons/android.svg", + "pages": [ + { + "group": "Overview", + "pages": [ + "calls/android/overview" + ] + }, + { + "group": "Getting Started", + "pages": [ + "calls/android/setup", + "calls/android/authentication", + "calls/android/join-session" + ] + }, + { + "group": "Call Session", + "pages": [ + "calls/android/actions", + "calls/android/events" + ] + }, + { + "group": "Features", + "pages": [ + "calls/android/ringing", + "calls/android/call-layouts", + "calls/android/audio-modes", + "calls/android/recording", + "calls/android/call-logs", + "calls/android/participant-management", + "calls/android/screen-sharing", + "calls/android/picture-in-picture", + "calls/android/raise-hand", + "calls/android/idle-timeout" + ] + } + ] + }, + { + "dropdown": "iOS", + "icon": "/images/icons/swift.svg", + "pages": [ + "calls/ios/overview" + ] + }, + { + "dropdown": "React Native", + "icon": "/images/icons/react.svg", + "pages": [ + "calls/react-native/overview" + ] + }, + { + "dropdown": "JavaScript", + "icon": "/images/icons/js.svg", + "pages": [ + "calls/javascript/overview" + ] + }, + { + "dropdown": "Flutter", + "icon": "/images/icons/flutter.svg", + "pages": [ + "calls/flutter/overview" + ] + }, + { + "dropdown": "Ionic", + "icon": "/images/icons/ionic.svg", + "pages": [ + "calls/ionic/overview" + ] + } + ] + }, + { + "tab": "API", + "tab-id": "calls-api", + "pages": [ + { + "group": "Calls", + "pages": [ + "calls/api/overview", + "calls/api/list-calls", + "calls/api/get-call" + ] + } + ] + } + ] + }, { "product": "AI Agents", "tabs": [ diff --git a/sdk/android/calls/session-settings.mdx b/sdk/android/calls/session-settings.mdx new file mode 100644 index 00000000..e69de29b From 4a214e9a6d32d9e6bf9e079f6a2542775563de98 Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Tue, 13 Jan 2026 16:14:30 +0530 Subject: [PATCH 02/10] docs(calls/android): remove redundant prerequisites sections - Remove duplicate prerequisites sections from all Android Calls SDK documentation files - Streamline documentation by eliminating repetitive prerequisite information - Improve readability and reduce cognitive load for developers - Prerequisites are now contextually referenced within relevant sections rather than listed at the top of each page --- calls/android/actions.mdx | 5 ----- calls/android/audio-modes.mdx | 6 ------ calls/android/authentication.mdx | 5 ----- calls/android/call-layouts.mdx | 5 ----- calls/android/call-logs.mdx | 5 ----- calls/android/events.mdx | 5 ----- calls/android/idle-timeout.mdx | 5 ----- calls/android/join-session.mdx | 6 ------ calls/android/participant-management.mdx | 6 ------ calls/android/picture-in-picture.mdx | 7 ------- calls/android/raise-hand.mdx | 6 ------ calls/android/recording.mdx | 6 ------ calls/android/ringing.mdx | 6 ------ calls/android/screen-sharing.mdx | 6 ------ calls/android/setup.mdx | 11 ----------- 15 files changed, 90 deletions(-) diff --git a/calls/android/actions.mdx b/calls/android/actions.mdx index a9530747..04f10bd9 100644 --- a/calls/android/actions.mdx +++ b/calls/android/actions.mdx @@ -5,11 +5,6 @@ sidebarTitle: "Actions" Use call actions to create your own custom controls or trigger call functionality dynamically based on your use case. All actions are called on the `CallSession` singleton instance during an active call session. -## Prerequisites - -- An active [call session](/calls/android/join-session) -- Access to the `CallSession` instance - ## Get CallSession Instance The `CallSession` is a singleton that manages the active call. All actions are accessed through this instance. diff --git a/calls/android/audio-modes.mdx b/calls/android/audio-modes.mdx index 96186d51..91f8dc11 100644 --- a/calls/android/audio-modes.mdx +++ b/calls/android/audio-modes.mdx @@ -5,12 +5,6 @@ sidebarTitle: "Audio Modes" Control audio output routing during calls. Switch between speaker, earpiece, Bluetooth, and wired headphones based on user preference or device availability. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- User [logged in](/calls/android/authentication) -- An active [call session](/calls/android/join-session) - ## Available Audio Modes | Mode | Description | diff --git a/calls/android/authentication.mdx b/calls/android/authentication.mdx index 4e0d2738..cf792192 100644 --- a/calls/android/authentication.mdx +++ b/calls/android/authentication.mdx @@ -5,11 +5,6 @@ sidebarTitle: "Authentication" Before users can make or receive calls, they must be authenticated with the CometChat Calls SDK. This guide covers the login and logout methods. -## Prerequisites - -- CometChat Calls SDK must be [initialized](/calls/android/setup) -- Users must exist in your CometChat app (create via [Dashboard](https://app.cometchat.com) or [API](https://api-explorer.cometchat.com/reference/creates-user)) - **Sample Users** diff --git a/calls/android/call-layouts.mdx b/calls/android/call-layouts.mdx index 4d2c8ac1..49d96683 100644 --- a/calls/android/call-layouts.mdx +++ b/calls/android/call-layouts.mdx @@ -5,11 +5,6 @@ sidebarTitle: "Call Layouts" Choose how participants are displayed during a call. The SDK provides multiple layout options to suit different use cases like team meetings, presentations, or one-on-one calls. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- User [logged in](/calls/android/authentication) - ## Available Layouts | Layout | Description | Best For | diff --git a/calls/android/call-logs.mdx b/calls/android/call-logs.mdx index 96887039..de1b1401 100644 --- a/calls/android/call-logs.mdx +++ b/calls/android/call-logs.mdx @@ -5,11 +5,6 @@ sidebarTitle: "Call Logs" Retrieve call history for your application. Call logs provide detailed information about past calls including duration, participants, recordings, and status. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- User [logged in](/calls/android/authentication) - ## Fetch Call Logs Use `CallLogRequest` to fetch call logs with pagination support. The builder pattern allows you to filter results by various criteria. diff --git a/calls/android/events.mdx b/calls/android/events.mdx index a698557f..d0566d50 100644 --- a/calls/android/events.mdx +++ b/calls/android/events.mdx @@ -5,11 +5,6 @@ sidebarTitle: "Events" Handle call session events to build responsive UIs. The SDK provides five event listener interfaces to monitor session status, participant activities, media changes, button clicks, and layout changes. Each listener is lifecycle-aware and automatically cleaned up when the Activity or Fragment is destroyed. -## Prerequisites - -- An active [call session](/calls/android/join-session) -- Access to the `CallSession` instance - ## Get CallSession Instance The `CallSession` is a singleton that manages the active call. All event listener registrations and session control methods are accessed through this instance. diff --git a/calls/android/idle-timeout.mdx b/calls/android/idle-timeout.mdx index 387f43c5..9efb4d9d 100644 --- a/calls/android/idle-timeout.mdx +++ b/calls/android/idle-timeout.mdx @@ -5,11 +5,6 @@ sidebarTitle: "Idle Timeout" Configure automatic session termination when a user is alone in a call. Idle timeout helps manage resources by ending sessions that have no active participants. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- User [logged in](/calls/android/authentication) - ## How Idle Timeout Works When a user is the only participant in a call session, the idle timeout countdown begins. If no other participant joins before the timeout expires, the session automatically ends and the `onSessionTimedOut` callback is triggered. diff --git a/calls/android/join-session.mdx b/calls/android/join-session.mdx index 28944957..3fa1ee72 100644 --- a/calls/android/join-session.mdx +++ b/calls/android/join-session.mdx @@ -5,12 +5,6 @@ sidebarTitle: "Join Session" Generate a token, configure settings, and join a call session. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- User [logged in](/calls/android/authentication) -- A container view (`RelativeLayout`) for the call UI - ## Generate Token A call token is required for secure access to a call session. Each token is unique to a specific session and user combination, ensuring that only authorized users can join the call. diff --git a/calls/android/participant-management.mdx b/calls/android/participant-management.mdx index 8f961222..a06256b1 100644 --- a/calls/android/participant-management.mdx +++ b/calls/android/participant-management.mdx @@ -9,12 +9,6 @@ Manage participants during a call with actions like muting, pausing video, and p By default, all participants who join a call have moderator access and can perform these actions. Implementing role-based moderation (e.g., restricting actions to hosts only) is the responsibility of the app developer based on their use case. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- User [logged in](/calls/android/authentication) -- An active [call session](/calls/android/join-session) - ## Mute a Participant Mute a specific participant's audio. This affects the participant for all users in the call. diff --git a/calls/android/picture-in-picture.mdx b/calls/android/picture-in-picture.mdx index c9302e27..492d4187 100644 --- a/calls/android/picture-in-picture.mdx +++ b/calls/android/picture-in-picture.mdx @@ -9,13 +9,6 @@ Enable Picture-in-Picture (PiP) mode to allow users to continue their call in a Picture-in-Picture implementation is handled at the app level using Android's PiP APIs. The Calls SDK only adjusts the call UI layout to fit the PiP window - it does not manage the PiP window itself. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- User [logged in](/calls/android/authentication) -- An active [call session](/calls/android/join-session) -- Android 8.0 (API level 26) or higher - ## How It Works 1. Your app enters PiP mode using Android's `enterPictureInPictureMode()` API diff --git a/calls/android/raise-hand.mdx b/calls/android/raise-hand.mdx index 1fd7cb27..c51bf596 100644 --- a/calls/android/raise-hand.mdx +++ b/calls/android/raise-hand.mdx @@ -5,12 +5,6 @@ sidebarTitle: "Raise Hand" Allow participants to raise their hand to get attention during calls. This feature is useful for large meetings, webinars, or any scenario where participants need to signal they want to speak. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- User [logged in](/calls/android/authentication) -- An active [call session](/calls/android/join-session) - ## Raise Hand Signal that you want to speak or get attention: diff --git a/calls/android/recording.mdx b/calls/android/recording.mdx index bf6e77d3..0f0dad66 100644 --- a/calls/android/recording.mdx +++ b/calls/android/recording.mdx @@ -5,12 +5,6 @@ sidebarTitle: "Recording" Record call sessions for later playback. Recordings are stored server-side and can be accessed through call logs or the CometChat Dashboard. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- User [logged in](/calls/android/authentication) -- Recording feature enabled for your CometChat app - Recording must be enabled for your CometChat app. Contact support or check your Dashboard settings if recording is not available. diff --git a/calls/android/ringing.mdx b/calls/android/ringing.mdx index 04c2534c..85e9b765 100644 --- a/calls/android/ringing.mdx +++ b/calls/android/ringing.mdx @@ -9,12 +9,6 @@ Implement incoming and outgoing call notifications with accept/reject functional Ringing functionality requires the CometChat Chat SDK to be integrated alongside the Calls SDK. The Chat SDK handles call signaling (initiating, accepting, rejecting calls), while the Calls SDK manages the actual call session. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- CometChat Chat SDK initialized and user logged in -- User [logged in](/calls/android/authentication) to Calls SDK - ## How Ringing Works The ringing flow involves two SDKs working together: diff --git a/calls/android/screen-sharing.mdx b/calls/android/screen-sharing.mdx index b9c4b125..9133f161 100644 --- a/calls/android/screen-sharing.mdx +++ b/calls/android/screen-sharing.mdx @@ -9,12 +9,6 @@ View screen shares from other participants during a call. The Android SDK can re The Android Calls SDK does not support initiating screen sharing. Screen sharing can only be started from web clients. Android participants can view shared screens. -## Prerequisites - -- CometChat Calls SDK [initialized](/calls/android/setup) -- User [logged in](/calls/android/authentication) -- An active [call session](/calls/android/join-session) - ## How It Works When a web participant starts screen sharing: diff --git a/calls/android/setup.mdx b/calls/android/setup.mdx index da122a55..9c7cf0d9 100644 --- a/calls/android/setup.mdx +++ b/calls/android/setup.mdx @@ -5,17 +5,6 @@ sidebarTitle: "Setup" This guide walks you through installing the CometChat Calls SDK and initializing it in your Android application. -## Prerequisites - -Before you begin, ensure you have: - -1. **CometChat Account**: [Sign up](https://app.cometchat.com/signup) and create an app -2. **Credentials**: Note your **App ID**, **Region**, and **API Key** from the CometChat Dashboard -3. **Android Requirements**: - - Minimum SDK: API Level 24 (Android 7.0) - - AndroidX compatibility - - Java 8 or higher - ## Add the CometChat Dependency ### Step 1: Add Repository From 50e9fb1aa074102734e2e630fcaec5cd4cb8b8c6 Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Wed, 14 Jan 2026 19:30:42 +0530 Subject: [PATCH 03/10] docs(calls/android): add advanced documentation for Android Calls SDK - Add SessionSettingsBuilder documentation with all configuration options - Add Custom Control Panel guide with MediaEventsListener integration - Add Custom Participant List guide with ParticipantEventListener - Add VoIP Calling documentation with ConnectionService implementation - Add Background Handling documentation for ongoing call service - Restructure Join Session page with clearer sections - Update navigation to include Advanced group with new pages --- calls/android/authentication.mdx | 8 - calls/android/background-handling.mdx | 478 +++++ calls/android/custom-control-panel.mdx | 660 +++++++ calls/android/custom-participant-list.mdx | 928 +++++++++ calls/android/join-session.mdx | 285 ++- calls/android/overview.mdx | 11 +- calls/android/session-settings.mdx | 686 +++++++ calls/android/voip-calling.mdx | 2151 +++++++++++++++++++++ docs.json | 14 +- 9 files changed, 5110 insertions(+), 111 deletions(-) create mode 100644 calls/android/background-handling.mdx create mode 100644 calls/android/custom-control-panel.mdx create mode 100644 calls/android/custom-participant-list.mdx create mode 100644 calls/android/session-settings.mdx create mode 100644 calls/android/voip-calling.mdx diff --git a/calls/android/authentication.mdx b/calls/android/authentication.mdx index cf792192..e2d53d89 100644 --- a/calls/android/authentication.mdx +++ b/calls/android/authentication.mdx @@ -213,11 +213,3 @@ Common authentication errors: | `ERROR_API_KEY_NOT_FOUND` | The API Key is missing or invalid | | `ERROR_BLANK_AUTHTOKEN` | The Auth Token is empty | | `ERROR_LOGIN_IN_PROGRESS` | A login operation is already in progress | - -## Next Steps - -Once authenticated, you can start or join call sessions. - - - Generate a token and join a call session - diff --git a/calls/android/background-handling.mdx b/calls/android/background-handling.mdx new file mode 100644 index 00000000..c507d0bd --- /dev/null +++ b/calls/android/background-handling.mdx @@ -0,0 +1,478 @@ +--- +title: "Background Handling" +sidebarTitle: "Background Handling" +--- + +Keep calls alive when users navigate away from your app. Background handling ensures the call continues running when users press the home button, switch to another app, or lock their device. + +## Overview + +When a user leaves your call activity, Android may terminate it to free resources. The SDK provides `CometChatOngoingCallService` - a foreground service that: +- Keeps the call session active in the background +- Shows an ongoing notification in the status bar +- Allows users to return to the call with a single tap +- Provides a hangup action directly from the notification + +```mermaid +flowchart LR + subgraph "User in Call" + A[Call Activity] --> B{User Action} + end + + B -->|Stays in app| A + B -->|Enters PiP| C[PiP Window] + B -->|Presses HOME| D[OngoingCallService] + B -->|Opens other app| D + + D --> E[Ongoing Notification] + E -->|Tap| A + E -->|Hangup| F[Call Ends] +``` + +## When to Use + +| Scenario | Solution | +|----------|----------| +| User stays in call activity | No action needed | +| User enters Picture-in-Picture | [PiP Mode](/calls/android/picture-in-picture) handles this | +| User presses HOME during call | **Use OngoingCallService** | +| User switches to another app | **Use OngoingCallService** | +| Receiving calls when app is killed | [VoIP Calling](/calls/android/voip-calling) handles this | + + +Background Handling is different from [VoIP Calling](/calls/android/voip-calling). VoIP handles **receiving** calls when the app is not running. Background Handling keeps an **active** call alive when the user leaves the app. + + +--- + +## Implementation + +### Step 1: Add Manifest Permissions + +Add the required permissions and service declaration to your `AndroidManifest.xml`: + +```xml + + + + + + + + + + +``` + +### Step 2: Start Service on Session Join + +Start the ongoing call service when the user successfully joins a call session: + + + +```kotlin +import com.cometchat.calls.services.CometChatOngoingCallService +import com.cometchat.calls.utils.OngoingNotification + +class CallActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + val callSession = CallSession.getInstance() + + callSession.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionJoined() { + // Start the foreground service to keep call alive in background + CometChatOngoingCallService.launch(this@CallActivity) + } + + override fun onSessionLeft() { + // Stop the service when call ends + CometChatOngoingCallService.abort(this@CallActivity) + finish() + } + + override fun onConnectionClosed() { + CometChatOngoingCallService.abort(this@CallActivity) + } + }) + + // Join the session... + } + + override fun onDestroy() { + // Always stop the service when activity is destroyed + CometChatOngoingCallService.abort(this) + super.onDestroy() + } +} +``` + + +```java +import com.cometchat.calls.services.CometChatOngoingCallService; +import com.cometchat.calls.utils.OngoingNotification; + +public class CallActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + CallSession callSession = CallSession.getInstance(); + + callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionJoined() { + // Start the foreground service to keep call alive in background + CometChatOngoingCallService.launch(CallActivity.this); + } + + @Override + public void onSessionLeft() { + // Stop the service when call ends + CometChatOngoingCallService.abort(CallActivity.this); + finish(); + } + + @Override + public void onConnectionClosed() { + CometChatOngoingCallService.abort(CallActivity.this); + } + }); + + // Join the session... + } + + @Override + protected void onDestroy() { + // Always stop the service when activity is destroyed + CometChatOngoingCallService.abort(this); + super.onDestroy(); + } +} +``` + + + +--- + +## Custom Notification + +Customize the ongoing call notification to match your app's branding: + + + +```kotlin +import com.cometchat.calls.utils.OngoingNotification + +private fun buildCustomNotification(): Notification { + val channelId = "CometChat_Call_Ongoing_Conference" + + // Intent to return to call when notification is tapped + val intent = Intent(this, CallActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + } + val pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.drawable.ic_call) + .setContentTitle("Ongoing Call") + .setContentText("Tap to return to your call") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setAutoCancel(false) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .build() +} + +// Use custom notification before launching service +callSession.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionJoined() { + OngoingNotification.buildOngoingConferenceNotification(buildCustomNotification()) + CometChatOngoingCallService.launch(this@CallActivity) + } +}) +``` + + +```java +import com.cometchat.calls.utils.OngoingNotification; + +private Notification buildCustomNotification() { + String channelId = "CometChat_Call_Ongoing_Conference"; + + // Intent to return to call when notification is tapped + Intent intent = new Intent(this, CallActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + PendingIntent pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + return new NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.drawable.ic_call) + .setContentTitle("Ongoing Call") + .setContentText("Tap to return to your call") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setAutoCancel(false) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .build(); +} + +// Use custom notification before launching service +callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionJoined() { + OngoingNotification.buildOngoingConferenceNotification(buildCustomNotification()); + CometChatOngoingCallService.launch(CallActivity.this); + } +}); +``` + + + + +The notification channel ID must be `CometChat_Call_Ongoing_Conference` to work with the SDK's service. + + +--- + +## Complete Example + + + +```kotlin +class CallActivity : AppCompatActivity() { + + private lateinit var callSession: CallSession + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + callSession = CallSession.getInstance() + + setupSessionListener() + joinCall() + } + + private fun setupSessionListener() { + callSession.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionJoined() { + // Build custom notification (optional) + OngoingNotification.buildOngoingConferenceNotification(buildCustomNotification()) + + // Start foreground service + CometChatOngoingCallService.launch(this@CallActivity) + } + + override fun onSessionLeft() { + CometChatOngoingCallService.abort(this@CallActivity) + finish() + } + + override fun onConnectionClosed() { + CometChatOngoingCallService.abort(this@CallActivity) + } + }) + } + + private fun joinCall() { + val sessionId = intent.getStringExtra("SESSION_ID") ?: return + val container = findViewById(R.id.callContainer) + + val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setTitle("My Call") + .build() + + CometChatCalls.joinSession( + sessionId = sessionId, + sessionSettings = sessionSettings, + view = container, + context = this, + listener = object : CometChatCalls.CallbackListener() { + override fun onSuccess(session: CallSession) { + Log.d(TAG, "Joined call successfully") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed to join: ${e.message}") + finish() + } + } + ) + } + + private fun buildCustomNotification(): Notification { + val channelId = "CometChat_Call_Ongoing_Conference" + + val intent = Intent(this, CallActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP + } + val pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + return NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.drawable.ic_call) + .setContentTitle("Ongoing Call") + .setContentText("Tap to return to your call") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setAutoCancel(false) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .build() + } + + override fun onDestroy() { + CometChatOngoingCallService.abort(this) + super.onDestroy() + } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + + private static final String TAG = "CallActivity"; + private CallSession callSession; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + callSession = CallSession.getInstance(); + + setupSessionListener(); + joinCall(); + } + + private void setupSessionListener() { + callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionJoined() { + // Build custom notification (optional) + OngoingNotification.buildOngoingConferenceNotification(buildCustomNotification()); + + // Start foreground service + CometChatOngoingCallService.launch(CallActivity.this); + } + + @Override + public void onSessionLeft() { + CometChatOngoingCallService.abort(CallActivity.this); + finish(); + } + + @Override + public void onConnectionClosed() { + CometChatOngoingCallService.abort(CallActivity.this); + } + }); + } + + private void joinCall() { + String sessionId = getIntent().getStringExtra("SESSION_ID"); + if (sessionId == null) return; + + FrameLayout container = findViewById(R.id.callContainer); + + SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setTitle("My Call") + .build(); + + CometChatCalls.joinSession( + sessionId, + sessionSettings, + container, + this, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession session) { + Log.d(TAG, "Joined call successfully"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to join: " + e.getMessage()); + finish(); + } + } + ); + } + + private Notification buildCustomNotification() { + String channelId = "CometChat_Call_Ongoing_Conference"; + + Intent intent = new Intent(this, CallActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); + PendingIntent pendingIntent = PendingIntent.getActivity( + this, 0, intent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + return new NotificationCompat.Builder(this, channelId) + .setSmallIcon(R.drawable.ic_call) + .setContentTitle("Ongoing Call") + .setContentText("Tap to return to your call") + .setPriority(NotificationCompat.PRIORITY_HIGH) + .setContentIntent(pendingIntent) + .setOngoing(true) + .setAutoCancel(false) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .build(); + } + + @Override + protected void onDestroy() { + CometChatOngoingCallService.abort(this); + super.onDestroy(); + } +} +``` + + + +--- + +## API Reference + +### CometChatOngoingCallService + +| Method | Description | +|--------|-------------| +| `launch(context)` | Starts the foreground service with an ongoing notification | +| `abort(context)` | Stops the foreground service and removes the notification | + +### OngoingNotification + +| Method | Description | +|--------|-------------| +| `buildOngoingConferenceNotification(notification)` | Sets a custom notification to display. Call before `launch()` | +| `createNotificationChannel(activity)` | Creates the notification channel (called automatically) | + +--- + +## Related Documentation + +- [Picture-in-Picture](/calls/android/picture-in-picture) - Keep call visible while using other apps +- [VoIP Calling](/calls/android/voip-calling) - Receive calls when app is killed +- [Session Status Listener](/calls/android/session-status-listener) - Listen for session events diff --git a/calls/android/custom-control-panel.mdx b/calls/android/custom-control-panel.mdx new file mode 100644 index 00000000..bdbc54ab --- /dev/null +++ b/calls/android/custom-control-panel.mdx @@ -0,0 +1,660 @@ +--- +title: "Custom Control Panel" +sidebarTitle: "Custom Control Panel" +--- + +Build a fully customized control panel for your call interface by hiding the default controls and implementing your own UI with call actions. This guide walks you through creating a custom control panel with essential call controls. + +## Overview + +Custom control panels allow you to: +- Match your app's branding and design language +- Simplify the interface by showing only relevant controls +- Add custom functionality and workflows +- Create unique user experiences + +This guide demonstrates building a basic custom control panel with: +- Mute/Unmute audio button +- Pause/Resume video button +- Switch camera button +- End call button + +## Prerequisites + +- CometChat Calls SDK installed and initialized +- Active call session (see [Join Session](/calls/android/join-session)) +- Familiarity with [Actions](/calls/android/actions) and [Events](/calls/android/events) + +--- + +## Step 1: Hide Default Controls + +Configure your session settings to hide the default control panel: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideControlPanel(true) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideControlPanel(true) + .build(); +``` + + + + +You can also hide individual buttons while keeping the control panel visible. See [SessionSettingsBuilder](/calls/android/session-settings) for all options. + + +--- + +## Step 2: Create Custom Layout + +Create an XML layout for your custom controls: + + + +```xml + + + + + + + + + + + + + + + + + + + + + + + +``` + + + +**control_button_background.xml:** +```xml + + + + +``` + +**end_call_button_background.xml:** +```xml + + + + +``` + + +--- + +## Step 3: Implement Control Actions + +Set up button click listeners and call the appropriate actions: + + + +```kotlin +class CallActivity : AppCompatActivity() { + + private lateinit var callSession: CallSession + private var isAudioMuted = false + private var isVideoPaused = false + + private lateinit var btnToggleAudio: ImageButton + private lateinit var btnToggleVideo: ImageButton + private lateinit var btnSwitchCamera: ImageButton + private lateinit var btnEndCall: ImageButton + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + callSession = CallSession.getInstance() + + btnToggleAudio = findViewById(R.id.btnToggleAudio) + btnToggleVideo = findViewById(R.id.btnToggleVideo) + btnSwitchCamera = findViewById(R.id.btnSwitchCamera) + btnEndCall = findViewById(R.id.btnEndCall) + + setupControlListeners() + } + + private fun setupControlListeners() { + btnToggleAudio.setOnClickListener { + if (isAudioMuted) { + callSession.unMuteAudio() + } else { + callSession.muteAudio() + } + } + + btnToggleVideo.setOnClickListener { + if (isVideoPaused) { + callSession.resumeVideo() + } else { + callSession.pauseVideo() + } + } + + btnSwitchCamera.setOnClickListener { + callSession.switchCamera() + } + + btnEndCall.setOnClickListener { + callSession.leaveSession() + finish() + } + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + + private CallSession callSession; + private boolean isAudioMuted = false; + private boolean isVideoPaused = false; + + private ImageButton btnToggleAudio; + private ImageButton btnToggleVideo; + private ImageButton btnSwitchCamera; + private ImageButton btnEndCall; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + callSession = CallSession.getInstance(); + + btnToggleAudio = findViewById(R.id.btnToggleAudio); + btnToggleVideo = findViewById(R.id.btnToggleVideo); + btnSwitchCamera = findViewById(R.id.btnSwitchCamera); + btnEndCall = findViewById(R.id.btnEndCall); + + setupControlListeners(); + } + + private void setupControlListeners() { + btnToggleAudio.setOnClickListener(v -> { + if (isAudioMuted) { + callSession.unMuteAudio(); + } else { + callSession.muteAudio(); + } + }); + + btnToggleVideo.setOnClickListener(v -> { + if (isVideoPaused) { + callSession.resumeVideo(); + } else { + callSession.pauseVideo(); + } + }); + + btnSwitchCamera.setOnClickListener(v -> callSession.switchCamera()); + + btnEndCall.setOnClickListener(v -> { + callSession.leaveSession(); + finish(); + }); + } +} +``` + + + + +--- + +## Step 4: Handle State Updates + +Use `MediaEventsListener` to keep your UI synchronized with the actual call state. The listener is lifecycle-aware and automatically removed when the Activity is destroyed. + + + +```kotlin +private fun setupMediaEventsListener() { + callSession.addMediaEventsListener(this, object : MediaEventsListener() { + override fun onAudioMuted() { + runOnUiThread { + isAudioMuted = true + btnToggleAudio.setImageResource(R.drawable.ic_mic_off) + } + } + + override fun onAudioUnMuted() { + runOnUiThread { + isAudioMuted = false + btnToggleAudio.setImageResource(R.drawable.ic_mic_on) + } + } + + override fun onVideoPaused() { + runOnUiThread { + isVideoPaused = true + btnToggleVideo.setImageResource(R.drawable.ic_video_off) + } + } + + override fun onVideoResumed() { + runOnUiThread { + isVideoPaused = false + btnToggleVideo.setImageResource(R.drawable.ic_video_on) + } + } + }) +} +``` + + +```java +private void setupMediaEventsListener() { + callSession.addMediaEventsListener(this, new MediaEventsListener() { + @Override + public void onAudioMuted() { + runOnUiThread(() -> { + isAudioMuted = true; + btnToggleAudio.setImageResource(R.drawable.ic_mic_off); + }); + } + + @Override + public void onAudioUnMuted() { + runOnUiThread(() -> { + isAudioMuted = false; + btnToggleAudio.setImageResource(R.drawable.ic_mic_on); + }); + } + + @Override + public void onVideoPaused() { + runOnUiThread(() -> { + isVideoPaused = true; + btnToggleVideo.setImageResource(R.drawable.ic_video_off); + }); + } + + @Override + public void onVideoResumed() { + runOnUiThread(() -> { + isVideoPaused = false; + btnToggleVideo.setImageResource(R.drawable.ic_video_on); + }); + } + }); +} +``` + + + +Use `SessionStatusListener` to handle session end events: + + + +```kotlin +private fun setupSessionStatusListener() { + callSession.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionLeft() { + runOnUiThread { finish() } + } + + override fun onConnectionClosed() { + runOnUiThread { finish() } + } + }) +} +``` + + +```java +private void setupSessionStatusListener() { + callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionLeft() { + runOnUiThread(() -> finish()); + } + + @Override + public void onConnectionClosed() { + runOnUiThread(() -> finish()); + } + }); +} +``` + + + +--- + +## Complete Example + +Here's the full implementation combining all steps: + + + +```kotlin +class CallActivity : AppCompatActivity() { + + private lateinit var callSession: CallSession + private var isAudioMuted = false + private var isVideoPaused = false + + private lateinit var btnToggleAudio: ImageButton + private lateinit var btnToggleVideo: ImageButton + private lateinit var btnSwitchCamera: ImageButton + private lateinit var btnEndCall: ImageButton + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + callSession = CallSession.getInstance() + + btnToggleAudio = findViewById(R.id.btnToggleAudio) + btnToggleVideo = findViewById(R.id.btnToggleVideo) + btnSwitchCamera = findViewById(R.id.btnSwitchCamera) + btnEndCall = findViewById(R.id.btnEndCall) + + setupControlListeners() + setupMediaEventsListener() + setupSessionStatusListener() + joinCall() + } + + private fun joinCall() { + val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .hideControlPanel(true) + .build() + + val callContainer = findViewById(R.id.callContainer) + + CometChatCalls.joinSession( + sessionId = "SESSION_ID", + sessionSettings = sessionSettings, + view = callContainer, + context = this, + listener = object : CometChatCalls.CallbackListener() { + override fun onSuccess(p0: Void?) { + Log.d(TAG, "Joined call successfully") + } + + override fun onError(exception: CometChatException) { + Log.e(TAG, "Failed to join: ${exception.message}") + finish() + } + } + ) + } + + private fun setupControlListeners() { + btnToggleAudio.setOnClickListener { + if (isAudioMuted) callSession.unMuteAudio() + else callSession.muteAudio() + } + + btnToggleVideo.setOnClickListener { + if (isVideoPaused) callSession.resumeVideo() + else callSession.pauseVideo() + } + + btnSwitchCamera.setOnClickListener { + callSession.switchCamera() + } + + btnEndCall.setOnClickListener { + callSession.leaveSession() + finish() + } + } + + private fun setupMediaEventsListener() { + callSession.addMediaEventsListener(this, object : MediaEventsListener() { + override fun onAudioMuted() { + runOnUiThread { + isAudioMuted = true + btnToggleAudio.setImageResource(R.drawable.ic_mic_off) + } + } + + override fun onAudioUnMuted() { + runOnUiThread { + isAudioMuted = false + btnToggleAudio.setImageResource(R.drawable.ic_mic_on) + } + } + + override fun onVideoPaused() { + runOnUiThread { + isVideoPaused = true + btnToggleVideo.setImageResource(R.drawable.ic_video_off) + } + } + + override fun onVideoResumed() { + runOnUiThread { + isVideoPaused = false + btnToggleVideo.setImageResource(R.drawable.ic_video_on) + } + } + }) + } + + private fun setupSessionStatusListener() { + callSession.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionLeft() { + runOnUiThread { finish() } + } + + override fun onConnectionClosed() { + runOnUiThread { finish() } + } + }) + } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + + private static final String TAG = "CallActivity"; + + private CallSession callSession; + private boolean isAudioMuted = false; + private boolean isVideoPaused = false; + + private ImageButton btnToggleAudio; + private ImageButton btnToggleVideo; + private ImageButton btnSwitchCamera; + private ImageButton btnEndCall; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + callSession = CallSession.getInstance(); + + btnToggleAudio = findViewById(R.id.btnToggleAudio); + btnToggleVideo = findViewById(R.id.btnToggleVideo); + btnSwitchCamera = findViewById(R.id.btnSwitchCamera); + btnEndCall = findViewById(R.id.btnEndCall); + + setupControlListeners(); + setupMediaEventsListener(); + setupSessionStatusListener(); + joinCall(); + } + + private void joinCall() { + SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .hideControlPanel(true) + .build(); + + FrameLayout callContainer = findViewById(R.id.callContainer); + + CometChatCalls.joinSession( + "SESSION_ID", + sessionSettings, + callContainer, + this, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(Void unused) { + Log.d(TAG, "Joined call successfully"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to join: " + e.getMessage()); + finish(); + } + } + ); + } + + private void setupControlListeners() { + btnToggleAudio.setOnClickListener(v -> { + if (isAudioMuted) callSession.unMuteAudio(); + else callSession.muteAudio(); + }); + + btnToggleVideo.setOnClickListener(v -> { + if (isVideoPaused) callSession.resumeVideo(); + else callSession.pauseVideo(); + }); + + btnSwitchCamera.setOnClickListener(v -> callSession.switchCamera()); + + btnEndCall.setOnClickListener(v -> { + callSession.leaveSession(); + finish(); + }); + } + + private void setupMediaEventsListener() { + callSession.addMediaEventsListener(this, new MediaEventsListener() { + @Override + public void onAudioMuted() { + runOnUiThread(() -> { + isAudioMuted = true; + btnToggleAudio.setImageResource(R.drawable.ic_mic_off); + }); + } + + @Override + public void onAudioUnMuted() { + runOnUiThread(() -> { + isAudioMuted = false; + btnToggleAudio.setImageResource(R.drawable.ic_mic_on); + }); + } + + @Override + public void onVideoPaused() { + runOnUiThread(() -> { + isVideoPaused = true; + btnToggleVideo.setImageResource(R.drawable.ic_video_off); + }); + } + + @Override + public void onVideoResumed() { + runOnUiThread(() -> { + isVideoPaused = false; + btnToggleVideo.setImageResource(R.drawable.ic_video_on); + }); + } + }); + } + + private void setupSessionStatusListener() { + callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionLeft() { + runOnUiThread(() -> finish()); + } + + @Override + public void onConnectionClosed() { + runOnUiThread(() -> finish()); + } + }); + } +} +``` + + diff --git a/calls/android/custom-participant-list.mdx b/calls/android/custom-participant-list.mdx new file mode 100644 index 00000000..6c793987 --- /dev/null +++ b/calls/android/custom-participant-list.mdx @@ -0,0 +1,928 @@ +--- +title: "Custom Participant List" +sidebarTitle: "Custom Participant List" +--- + +Build a custom participant list UI that displays real-time participant information with full control over layout and interactions. This guide demonstrates how to hide the default participant list and create your own using participant events and actions. + +## Overview + +The SDK provides participant data through events, allowing you to build custom UIs for: +- Participant roster with search and filtering +- Custom participant cards with role badges or metadata +- Moderation dashboards with quick access to controls +- Attendance tracking and engagement monitoring + +## Prerequisites + +- CometChat Calls SDK installed and initialized +- Active call session (see [Join Session](/calls/android/join-session)) +- Basic understanding of RecyclerView and adapters + +--- + +## Step 1: Hide Default Participant List + +Configure session settings to hide the default participant list button: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideParticipantListButton(true) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideParticipantListButton(true) + .build(); +``` + + + + +--- + +## Step 2: Create Participant List Layout + +Create a layout with RecyclerView for displaying participants: + + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + + + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + + + +--- + +## Step 3: Create Participant Adapter + +Build a RecyclerView adapter to display participant data: + + + +```kotlin +class ParticipantAdapter( + private val onMuteClick: (Participant) -> Unit, + private val onPauseVideoClick: (Participant) -> Unit, + private val onPinClick: (Participant) -> Unit +) : RecyclerView.Adapter() { + + private var participants = listOf() + private var filteredParticipants = listOf() + + class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val avatar: ImageView = view.findViewById(R.id.participantAvatar) + val name: TextView = view.findViewById(R.id.participantName) + val status: TextView = view.findViewById(R.id.statusIndicator) + val muteButton: ImageButton = view.findViewById(R.id.muteButton) + val videoPauseButton: ImageButton = view.findViewById(R.id.videoPauseButton) + val pinButton: ImageButton = view.findViewById(R.id.pinButton) + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.item_participant, parent, false) + return ViewHolder(view) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val participant = filteredParticipants[position] + + // Set name + holder.name.text = participant.name + + // Load avatar (use your image loading library) + // Glide.with(holder.avatar).load(participant.avatar).into(holder.avatar) + + // Build status text + val statusParts = mutableListOf() + if (participant.isAudioMuted) statusParts.add("🔇 Muted") + if (participant.isVideoPaused) statusParts.add("📹 Video Off") + if (participant.isPresenting) statusParts.add("🖥️ Presenting") + if (participant.raisedHandTimestamp > 0) statusParts.add("✋ Hand Raised") + if (participant.isPinned) statusParts.add("📌 Pinned") + + holder.status.text = if (statusParts.isEmpty()) "Active" else statusParts.joinToString(" • ") + + // Action buttons + holder.muteButton.setOnClickListener { onMuteClick(participant) } + holder.videoPauseButton.setOnClickListener { onPauseVideoClick(participant) } + holder.pinButton.setOnClickListener { onPinClick(participant) } + + // Update button states + holder.muteButton.alpha = if (participant.isAudioMuted) 0.5f else 1.0f + holder.videoPauseButton.alpha = if (participant.isVideoPaused) 0.5f else 1.0f + holder.pinButton.alpha = if (participant.isPinned) 1.0f else 0.5f + } + + override fun getItemCount() = filteredParticipants.size + + fun updateParticipants(newParticipants: List) { + participants = newParticipants + filteredParticipants = newParticipants + notifyDataSetChanged() + } + + fun filter(query: String) { + filteredParticipants = if (query.isEmpty()) { + participants + } else { + participants.filter { + it.name.contains(query, ignoreCase = true) + } + } + notifyDataSetChanged() + } +} +``` + + +```java +public class ParticipantAdapter extends RecyclerView.Adapter { + + private List participants = new ArrayList<>(); + private List filteredParticipants = new ArrayList<>(); + private final OnActionListener listener; + + public interface OnActionListener { + void onMuteClick(Participant participant); + void onPauseVideoClick(Participant participant); + void onPinClick(Participant participant); + } + + public ParticipantAdapter(OnActionListener listener) { + this.listener = listener; + } + + static class ViewHolder extends RecyclerView.ViewHolder { + ImageView avatar; + TextView name; + TextView status; + ImageButton muteButton; + ImageButton videoPauseButton; + ImageButton pinButton; + + ViewHolder(View view) { + super(view); + avatar = view.findViewById(R.id.participantAvatar); + name = view.findViewById(R.id.participantName); + status = view.findViewById(R.id.statusIndicator); + muteButton = view.findViewById(R.id.muteButton); + videoPauseButton = view.findViewById(R.id.videoPauseButton); + pinButton = view.findViewById(R.id.pinButton); + } + } + + @Override + public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_participant, parent, false); + return new ViewHolder(view); + } + + @Override + public void onBindViewHolder(ViewHolder holder, int position) { + Participant participant = filteredParticipants.get(position); + + // Set name + holder.name.setText(participant.getName()); + + // Load avatar (use your image loading library) + // Glide.with(holder.avatar).load(participant.getAvatar()).into(holder.avatar); + + // Build status text + List statusParts = new ArrayList<>(); + if (participant.isAudioMuted()) statusParts.add("🔇 Muted"); + if (participant.isVideoPaused()) statusParts.add("📹 Video Off"); + if (participant.isPresenting()) statusParts.add("🖥️ Presenting"); + if (participant.getRaisedHandTimestamp() > 0) statusParts.add("✋ Hand Raised"); + if (participant.isPinned()) statusParts.add("📌 Pinned"); + + holder.status.setText(statusParts.isEmpty() ? "Active" : String.join(" • ", statusParts)); + + // Action buttons + holder.muteButton.setOnClickListener(v -> listener.onMuteClick(participant)); + holder.videoPauseButton.setOnClickListener(v -> listener.onPauseVideoClick(participant)); + holder.pinButton.setOnClickListener(v -> listener.onPinClick(participant)); + + // Update button states + holder.muteButton.setAlpha(participant.isAudioMuted() ? 0.5f : 1.0f); + holder.videoPauseButton.setAlpha(participant.isVideoPaused() ? 0.5f : 1.0f); + holder.pinButton.setAlpha(participant.isPinned() ? 1.0f : 0.5f); + } + + @Override + public int getItemCount() { + return filteredParticipants.size(); + } + + public void updateParticipants(List newParticipants) { + participants = new ArrayList<>(newParticipants); + filteredParticipants = new ArrayList<>(newParticipants); + notifyDataSetChanged(); + } + + public void filter(String query) { + if (query.isEmpty()) { + filteredParticipants = new ArrayList<>(participants); + } else { + filteredParticipants = new ArrayList<>(); + for (Participant p : participants) { + if (p.getName().toLowerCase().contains(query.toLowerCase())) { + filteredParticipants.add(p); + } + } + } + notifyDataSetChanged(); + } +} +``` + + + + +--- + +## Step 4: Implement Participant Events + +Listen for participant updates and handle actions in your Activity: + + + +```kotlin +class CallActivity : AppCompatActivity() { + + private lateinit var participantAdapter: ParticipantAdapter + private lateinit var callSession: CallSession + private var isParticipantPanelVisible = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + callSession = CallSession.getInstance() + + // Setup RecyclerView + val recyclerView = findViewById(R.id.participantRecyclerView) + recyclerView.layoutManager = LinearLayoutManager(this) + + participantAdapter = ParticipantAdapter( + onMuteClick = { participant -> + callSession.muteParticipant(participant.uid) + }, + onPauseVideoClick = { participant -> + callSession.pauseParticipantVideo(participant.uid) + }, + onPinClick = { participant -> + if (participant.isPinned) { + callSession.unPinParticipant() + } else { + callSession.pinParticipant(participant.uid) + } + } + ) + recyclerView.adapter = participantAdapter + + // Setup search + val searchInput = findViewById(R.id.searchInput) + searchInput.addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + participantAdapter.filter(s.toString()) + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + + // Setup toggle button + findViewById(R.id.toggleParticipantListButton).setOnClickListener { + toggleParticipantPanel() + } + + // Setup close button + findViewById(R.id.closeButton).setOnClickListener { + toggleParticipantPanel() + } + + // Listen for participant events + setupParticipantListener() + } + + private fun setupParticipantListener() { + callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantListChanged(participants: List) { + runOnUiThread { + participantAdapter.updateParticipants(participants) + updateParticipantCount(participants.size) + } + } + + override fun onParticipantJoined(participant: Participant) { + Log.d(TAG, "${participant.name} joined") + } + + override fun onParticipantLeft(participant: Participant) { + Log.d(TAG, "${participant.name} left") + } + + override fun onParticipantAudioMuted(participant: Participant) { + // Adapter will update automatically via onParticipantListChanged + } + + override fun onParticipantAudioUnmuted(participant: Participant) { + // Adapter will update automatically via onParticipantListChanged + } + + override fun onParticipantVideoPaused(participant: Participant) { + // Adapter will update automatically via onParticipantListChanged + } + + override fun onParticipantVideoResumed(participant: Participant) { + // Adapter will update automatically via onParticipantListChanged + } + + override fun onParticipantHandRaised(participant: Participant) { + // Adapter will update automatically via onParticipantListChanged + } + + override fun onParticipantHandLowered(participant: Participant) { + // Adapter will update automatically via onParticipantListChanged + } + }) + } + + private fun toggleParticipantPanel() { + val panel = findViewById(R.id.participantPanel) + isParticipantPanelVisible = !isParticipantPanelVisible + panel.visibility = if (isParticipantPanelVisible) View.VISIBLE else View.GONE + } + + private fun updateParticipantCount(count: Int) { + findViewById(R.id.participantCount).text = "Participants ($count)" + } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + + private ParticipantAdapter participantAdapter; + private CallSession callSession; + private boolean isParticipantPanelVisible = false; + private static final String TAG = "CallActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + callSession = CallSession.getInstance(); + + // Setup RecyclerView + RecyclerView recyclerView = findViewById(R.id.participantRecyclerView); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + + participantAdapter = new ParticipantAdapter(new ParticipantAdapter.OnActionListener() { + @Override + public void onMuteClick(Participant participant) { + callSession.muteParticipant(participant.getUid()); + } + + @Override + public void onPauseVideoClick(Participant participant) { + callSession.pauseParticipantVideo(participant.getUid()); + } + + @Override + public void onPinClick(Participant participant) { + if (participant.isPinned()) { + callSession.unPinParticipant(); + } else { + callSession.pinParticipant(participant.getUid()); + } + } + }); + recyclerView.setAdapter(participantAdapter); + + // Setup search + EditText searchInput = findViewById(R.id.searchInput); + searchInput.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + participantAdapter.filter(s.toString()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + }); + + // Setup toggle button + findViewById(R.id.toggleParticipantListButton).setOnClickListener(v -> toggleParticipantPanel()); + + // Setup close button + findViewById(R.id.closeButton).setOnClickListener(v -> toggleParticipantPanel()); + + // Listen for participant events + setupParticipantListener(); + } + + private void setupParticipantListener() { + callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantListChanged(List participants) { + runOnUiThread(() -> { + participantAdapter.updateParticipants(participants); + updateParticipantCount(participants.size()); + }); + } + + @Override + public void onParticipantJoined(Participant participant) { + Log.d(TAG, participant.getName() + " joined"); + } + + @Override + public void onParticipantLeft(Participant participant) { + Log.d(TAG, participant.getName() + " left"); + } + + @Override + public void onParticipantAudioMuted(Participant participant) { + // Adapter will update automatically via onParticipantListChanged + } + + @Override + public void onParticipantAudioUnmuted(Participant participant) { + // Adapter will update automatically via onParticipantListChanged + } + + @Override + public void onParticipantVideoPaused(Participant participant) { + // Adapter will update automatically via onParticipantListChanged + } + + @Override + public void onParticipantVideoResumed(Participant participant) { + // Adapter will update automatically via onParticipantListChanged + } + + @Override + public void onParticipantHandRaised(Participant participant) { + // Adapter will update automatically via onParticipantListChanged + } + + @Override + public void onParticipantHandLowered(Participant participant) { + // Adapter will update automatically via onParticipantListChanged + } + }); + } + + private void toggleParticipantPanel() { + LinearLayout panel = findViewById(R.id.participantPanel); + isParticipantPanelVisible = !isParticipantPanelVisible; + panel.setVisibility(isParticipantPanelVisible ? View.VISIBLE : View.GONE); + } + + private void updateParticipantCount(int count) { + TextView countView = findViewById(R.id.participantCount); + countView.setText("Participants (" + count + ")"); + } +} +``` + + + + +--- + +## Complete Example + +Here's the full implementation with all components: + + + +```kotlin +class CallActivity : AppCompatActivity() { + + private lateinit var participantAdapter: ParticipantAdapter + private lateinit var callSession: CallSession + private var isParticipantPanelVisible = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + callSession = CallSession.getInstance() + setupUI() + setupParticipantListener() + joinCall() + } + + private fun setupUI() { + // Setup RecyclerView + val recyclerView = findViewById(R.id.participantRecyclerView) + recyclerView.layoutManager = LinearLayoutManager(this) + + participantAdapter = ParticipantAdapter( + onMuteClick = { callSession.muteParticipant(it.uid) }, + onPauseVideoClick = { callSession.pauseParticipantVideo(it.uid) }, + onPinClick = { + if (it.isPinned) callSession.unPinParticipant() + else callSession.pinParticipant(it.uid) + } + ) + recyclerView.adapter = participantAdapter + + // Setup search + findViewById(R.id.searchInput).addTextChangedListener(object : TextWatcher { + override fun afterTextChanged(s: Editable?) { + participantAdapter.filter(s.toString()) + } + override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} + override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} + }) + + // Setup buttons + findViewById(R.id.toggleParticipantListButton) + .setOnClickListener { toggleParticipantPanel() } + findViewById(R.id.closeButton) + .setOnClickListener { toggleParticipantPanel() } + } + + private fun setupParticipantListener() { + callSession.addParticipantEventListener(this, object : ParticipantEventListener() { + override fun onParticipantListChanged(participants: List) { + runOnUiThread { + participantAdapter.updateParticipants(participants) + findViewById(R.id.participantCount).text = + "Participants (${participants.size})" + } + } + + override fun onParticipantJoined(participant: Participant) { + Toast.makeText(this@CallActivity, + "${participant.name} joined", Toast.LENGTH_SHORT).show() + } + + override fun onParticipantLeft(participant: Participant) { + Toast.makeText(this@CallActivity, + "${participant.name} left", Toast.LENGTH_SHORT).show() + } + }) + } + + private fun joinCall() { + val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideParticipantListButton(true) + .setTitle("Team Meeting") + .build() + + val callContainer = findViewById(R.id.callContainer) + + CometChatCalls.joinSession( + sessionId = "SESSION_ID", + sessionSettings = sessionSettings, + view = callContainer, + context = this, + listener = object : CometChatCalls.CallbackListener() { + override fun onSuccess(p0: Void?) { + Log.d(TAG, "Joined call successfully") + } + + override fun onError(exception: CometChatException) { + Log.e(TAG, "Failed to join: ${exception.message}") + Toast.makeText(this@CallActivity, + "Failed to join call", Toast.LENGTH_SHORT).show() + } + } + ) + } + + private fun toggleParticipantPanel() { + val panel = findViewById(R.id.participantPanel) + isParticipantPanelVisible = !isParticipantPanelVisible + panel.visibility = if (isParticipantPanelVisible) View.VISIBLE else View.GONE + } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + + private ParticipantAdapter participantAdapter; + private CallSession callSession; + private boolean isParticipantPanelVisible = false; + private static final String TAG = "CallActivity"; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + callSession = CallSession.getInstance(); + setupUI(); + setupParticipantListener(); + joinCall(); + } + + private void setupUI() { + // Setup RecyclerView + RecyclerView recyclerView = findViewById(R.id.participantRecyclerView); + recyclerView.setLayoutManager(new LinearLayoutManager(this)); + + participantAdapter = new ParticipantAdapter(new ParticipantAdapter.OnActionListener() { + @Override + public void onMuteClick(Participant participant) { + callSession.muteParticipant(participant.getUid()); + } + + @Override + public void onPauseVideoClick(Participant participant) { + callSession.pauseParticipantVideo(participant.getUid()); + } + + @Override + public void onPinClick(Participant participant) { + if (participant.isPinned()) { + callSession.unPinParticipant(); + } else { + callSession.pinParticipant(participant.getUid()); + } + } + }); + recyclerView.setAdapter(participantAdapter); + + // Setup search + EditText searchInput = findViewById(R.id.searchInput); + searchInput.addTextChangedListener(new TextWatcher() { + @Override + public void afterTextChanged(Editable s) { + participantAdapter.filter(s.toString()); + } + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) {} + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + }); + + // Setup buttons + findViewById(R.id.toggleParticipantListButton).setOnClickListener(v -> + toggleParticipantPanel()); + findViewById(R.id.closeButton).setOnClickListener(v -> + toggleParticipantPanel()); + } + + private void setupParticipantListener() { + callSession.addParticipantEventListener(this, new ParticipantEventListener() { + @Override + public void onParticipantListChanged(List participants) { + runOnUiThread(() -> { + participantAdapter.updateParticipants(participants); + TextView countView = findViewById(R.id.participantCount); + countView.setText("Participants (" + participants.size() + ")"); + }); + } + + @Override + public void onParticipantJoined(Participant participant) { + Toast.makeText(CallActivity.this, + participant.getName() + " joined", Toast.LENGTH_SHORT).show(); + } + + @Override + public void onParticipantLeft(Participant participant) { + Toast.makeText(CallActivity.this, + participant.getName() + " left", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void joinCall() { + SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideParticipantListButton(true) + .setTitle("Team Meeting") + .build(); + + FrameLayout callContainer = findViewById(R.id.callContainer); + + CometChatCalls.joinSession( + "SESSION_ID", + sessionSettings, + callContainer, + this, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(Void unused) { + Log.d(TAG, "Joined call successfully"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to join: " + e.getMessage()); + Toast.makeText(CallActivity.this, + "Failed to join call", Toast.LENGTH_SHORT).show(); + } + } + ); + } + + private void toggleParticipantPanel() { + LinearLayout panel = findViewById(R.id.participantPanel); + isParticipantPanelVisible = !isParticipantPanelVisible; + panel.setVisibility(isParticipantPanelVisible ? View.VISIBLE : View.GONE); + } +} +``` + + + + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | String | Unique identifier (CometChat user ID) | +| `name` | String | Display name | +| `avatar` | String | URL of avatar image | +| `pid` | String | Participant ID for this call session | +| `role` | String | Role in the call | +| `audioMuted` | Boolean | Whether audio is muted | +| `videoPaused` | Boolean | Whether video is paused | +| `isPinned` | Boolean | Whether pinned in layout | +| `isPresenting` | Boolean | Whether screen sharing | +| `raisedHandTimestamp` | Long | Timestamp when hand was raised (0 if not raised) | + diff --git a/calls/android/join-session.mdx b/calls/android/join-session.mdx index 3fa1ee72..199d84fa 100644 --- a/calls/android/join-session.mdx +++ b/calls/android/join-session.mdx @@ -3,47 +3,85 @@ title: "Join Session" sidebarTitle: "Join Session" --- -Generate a token, configure settings, and join a call session. +Join a call session using one of two approaches: the quick start method with a session ID, or the advanced flow with manual token generation for more control. -## Generate Token +## Overview -A call token is required for secure access to a call session. Each token is unique to a specific session and user combination, ensuring that only authorized users can join the call. +The CometChat Calls SDK provides two ways to join a session: -You can generate the token just before starting the call, or generate and store it ahead of time based on your use case. +| Approach | Best For | Complexity | +|----------|----------|------------| +| **Join with Session ID** | Most use cases - simple and straightforward | Low - One method call | +| **Join with Token** | Custom token management, pre-generation, caching | Medium - Two-step process | -Use the `generateToken()` method to create a call token: + +Both approaches require a container view in your layout and properly configured [SessionSettings](/calls/android/session-settings). + + +## Container Setup + +Add a container view to your layout where the call interface will be rendered: + +```xml + +``` + +The call UI will be dynamically added to this container when you join the session. + +## Join with Session ID + +The simplest way to join a session. Pass a session ID and the SDK automatically generates the token and joins the call. ```kotlin val sessionId = "SESSION_ID" +val callViewContainer = findViewById(R.id.call_view_container) -CometChatCalls.generateToken(sessionId, object : CometChatCalls.CallbackListener() { - override fun onSuccess(token: GenerateToken) { - Log.d(TAG, "Token: ${token.token}") - } +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .build() - override fun onError(e: CometChatException) { - Log.e(TAG, "Error: ${e.message}") +CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + object : CometChatCalls.CallbackListener() { + override fun onSuccess(callSession: CallSession) { + Log.d(TAG, "Joined session successfully") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed: ${e.message}") + } } -}) +) ``` ```java String sessionId = "SESSION_ID"; +RelativeLayout callViewContainer = findViewById(R.id.call_view_container); -CometChatCalls.generateToken(sessionId, new CometChatCalls.CallbackListener() { - @Override - public void onSuccess(GenerateToken token) { - Log.d(TAG, "Token: " + token.getToken()); - } +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .build(); - @Override - public void onError(CometChatException e) { - Log.e(TAG, "Error: " + e.getMessage()); +CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession callSession) { + Log.d(TAG, "Joined session successfully"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed: " + e.getMessage()); + } } -}); +); ``` @@ -51,104 +89,71 @@ CometChatCalls.generateToken(sessionId, new CometChatCalls.CallbackListener All participants joining the same call must use the same session ID. -## Session Settings +## Join with Token + +For scenarios requiring more control over token generation, such as pre-generating tokens, implementing custom caching strategies, or managing token lifecycle separately. -Configure how a call session behaves and appears using `SessionSettingsBuilder`. +**Step 1: Generate Token** + +Generate a call token for the session. Each token is unique to a specific session and user combination. ```kotlin -val sessionSettings = CometChatCalls.SessionSettingsBuilder() - .setTitle("Team Meeting") - .setDisplayName("John Doe") - .setType(SessionType.VIDEO) - .setLayout(LayoutType.TILE) - .startAudioMuted(false) - .startVideoPaused(false) - .build() +val sessionId = "SESSION_ID" + +CometChatCalls.generateToken(sessionId, object : CometChatCalls.CallbackListener() { + override fun onSuccess(token: GenerateToken) { + Log.d(TAG, "Token generated: ${token.token}") + // Store or use the token + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Token generation failed: ${e.message}") + } +}) ``` ```java -SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() - .setTitle("Team Meeting") - .setDisplayName("John Doe") - .setType(SessionType.VIDEO) - .setLayout(LayoutType.TILE) - .startAudioMuted(false) - .startVideoPaused(false) - .build(); +String sessionId = "SESSION_ID"; + +CometChatCalls.generateToken(sessionId, new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(GenerateToken token) { + Log.d(TAG, "Token generated: " + token.getToken()); + // Store or use the token + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Token generation failed: " + e.getMessage()); + } +}); ``` -| Method | Type | Default | Description | -|--------|------|---------|-------------| -| `setTitle(String)` | String | null | Title displayed in the call header | -| `setDisplayName(String)` | String | null | Display name shown to other participants | -| `setType(SessionType)` | SessionType | VIDEO | Call type: `VIDEO` or `AUDIO` | -| `setLayout(LayoutType)` | LayoutType | TILE | Layout: `TILE`, `SPOTLIGHT`, or `SIDEBAR` | -| `setIdleTimeoutPeriod(int)` | int | 300 | Timeout in seconds when alone in the session | -| `startAudioMuted(boolean)` | boolean | false | Start with microphone muted | -| `startVideoPaused(boolean)` | boolean | false | Start with camera off | -| `setAudioMode(AudioMode)` | AudioMode | SPEAKER | Initial audio output device | -| `setInitialCameraFacing(CameraFacing)` | CameraFacing | FRONT | Initial camera: `FRONT` or `BACK` | -| `enableLowBandwidthMode(boolean)` | boolean | false | Optimize for poor network conditions | -| `enableAutoStartRecording(boolean)` | boolean | false | Auto-start recording when session begins | -| `hideControlPanel(boolean)` | boolean | false | Hide bottom control bar | -| `hideHeaderPanel(boolean)` | boolean | false | Hide top header bar | -| `hideSessionTimer(boolean)` | boolean | false | Hide session duration timer | -| `hideLeaveSessionButton(boolean)` | boolean | false | Hide leave/end call button | -| `hideToggleAudioButton(boolean)` | boolean | false | Hide mute/unmute button | -| `hideToggleVideoButton(boolean)` | boolean | false | Hide video on/off button | -| `hideSwitchCameraButton(boolean)` | boolean | false | Hide camera flip button | -| `hideRecordingButton(boolean)` | boolean | true | Hide recording button | -| `hideScreenSharingButton(boolean)` | boolean | false | Hide screen share button | -| `hideAudioModeButton(boolean)` | boolean | false | Hide speaker/earpiece toggle | -| `hideRaiseHandButton(boolean)` | boolean | false | Hide raise hand button | -| `hideShareInviteButton(boolean)` | boolean | true | Hide share invite button | -| `hideParticipantListButton(boolean)` | boolean | false | Hide participant list button | -| `hideChangeLayoutButton(boolean)` | boolean | false | Hide layout toggle button | -| `hideChatButton(boolean)` | boolean | true | Hide in-call chat button | - - -| Enum | Value | Description | -|------|-------|-------------| -| `SessionType` | `VIDEO` | Video call with camera enabled | -| | `AUDIO` | Audio-only call | -| `LayoutType` | `TILE` | Grid layout showing all participants equally | -| | `SPOTLIGHT` | Focus on active speaker with others in sidebar | -| | `SIDEBAR` | Main speaker with participants in a sidebar | -| `AudioMode` | `SPEAKER` | Device loudspeaker | -| | `EARPIECE` | Phone earpiece | -| | `BLUETOOTH` | Connected Bluetooth device | -| | `HEADPHONES` | Wired headphones | -| `CameraFacing` | `FRONT` | Front-facing camera | -| | `BACK` | Rear camera | - - -## Join Session - -Add a container view to your layout: +| Parameter | Type | Description | +|-----------|------|-------------| +| `sessionId` | String | Unique identifier for the call session | +| `listener` | CallbackListener | Callback returning the generated token | -```xml - -``` +**Step 2: Join with Token** -Use the `joinSession()` method to join a call session: +Use the generated token to join the session. This gives you control over when and how the token is used. ```kotlin -val sessionId = "SESSION_ID" val callViewContainer = findViewById(R.id.call_view_container) val sessionSettings = CometChatCalls.SessionSettingsBuilder() @@ -156,7 +161,8 @@ val sessionSettings = CometChatCalls.SessionSettingsBuilder() .setType(SessionType.VIDEO) .build() -CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, +// Use the previously generated token +CometChatCalls.joinSession(generatedToken, sessionSettings, callViewContainer, object : CometChatCalls.CallbackListener() { override fun onSuccess(callSession: CallSession) { Log.d(TAG, "Joined session successfully") @@ -171,7 +177,6 @@ CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, ```java -String sessionId = "SESSION_ID"; RelativeLayout callViewContainer = findViewById(R.id.call_view_container); SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() @@ -179,7 +184,8 @@ SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() .setType(SessionType.VIDEO) .build(); -CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, +// Use the previously generated token +CometChatCalls.joinSession(generatedToken, sessionSettings, callViewContainer, new CometChatCalls.CallbackListener() { @Override public void onSuccess(CallSession callSession) { @@ -198,16 +204,95 @@ CometChatCalls.joinSession(sessionId, sessionSettings, callViewContainer, | Parameter | Type | Description | |-----------|------|-------------| -| `sessionId` | String | Unique session identifier (or use `GenerateToken`) | +| `callToken` | GenerateToken | Previously generated token object | | `sessionSettings` | SessionSettings | Configuration for the session | | `callViewContainer` | RelativeLayout | Container view for the call UI | +| `listener` | CallbackListener | Callback for success/error handling | + +**Complete Example** + + + +```kotlin +val sessionId = "SESSION_ID" +val callViewContainer = findViewById(R.id.call_view_container) + +// Step 1: Generate token +CometChatCalls.generateToken(sessionId, object : CometChatCalls.CallbackListener() { + override fun onSuccess(token: GenerateToken) { + // Step 2: Join with token + val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .build() + + CometChatCalls.joinSession(token, sessionSettings, callViewContainer, + object : CometChatCalls.CallbackListener() { + override fun onSuccess(callSession: CallSession) { + Log.d(TAG, "Joined session successfully") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed to join: ${e.message}") + } + } + ) + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Token generation failed: ${e.message}") + } +}) +``` + + +```java +String sessionId = "SESSION_ID"; +RelativeLayout callViewContainer = findViewById(R.id.call_view_container); + +// Step 1: Generate token +CometChatCalls.generateToken(sessionId, new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(GenerateToken token) { + // Step 2: Join with token + SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .build(); + + CometChatCalls.joinSession(token, sessionSettings, callViewContainer, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession callSession) { + Log.d(TAG, "Joined session successfully"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to join: " + e.getMessage()); + } + } + ); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Token generation failed: " + e.getMessage()); + } +}); +``` + + ## Error Handling +Common errors when joining a session: + | Error Code | Description | |------------|-------------| -| `ERROR_COMETCHAT_CALLS_SDK_INIT` | SDK not initialized | +| `ERROR_COMETCHAT_CALLS_SDK_INIT` | SDK not initialized - call `init()` first | | `ERROR_AUTH_TOKEN` | User not logged in or auth token invalid | | `ERROR_CALL_SESSION_ID` | Session ID is null or empty | | `ERROR_CALL_TOKEN` | Invalid or missing call token | | `ERROR_CALLING_VIEW_REF_NULL` | Container view is null | +| `ERROR_JSON_EXCEPTION` | Invalid session settings or response parsing error | diff --git a/calls/android/overview.mdx b/calls/android/overview.mdx index dffd190d..a1c0bb07 100644 --- a/calls/android/overview.mdx +++ b/calls/android/overview.mdx @@ -6,7 +6,16 @@ sidebarTitle: "Overview" The CometChat Calls SDK enables real-time voice and video calling capabilities in your Android application. Built on top of WebRTC, it provides a complete calling solution with built-in UI components and extensive customization options. -Using CometChat UI Kits? Calling is already integrated. See [Android UI Kit](/ui-kit/android/call-features) for a faster integration path. +**Faster Integration with UI Kits** + +If you're using CometChat UI Kits, voice and video calling is already integrated with ready-to-use components: +- Incoming & outgoing call screens +- Call buttons with one-tap calling +- Call logs with history + +👉 [Android UI Kit Call Features](/ui-kit/android/call-features) + +Use this Calls SDK directly only if you need custom call UI or advanced control. ## Prerequisites diff --git a/calls/android/session-settings.mdx b/calls/android/session-settings.mdx new file mode 100644 index 00000000..24272810 --- /dev/null +++ b/calls/android/session-settings.mdx @@ -0,0 +1,686 @@ +--- +title: "SessionSettingsBuilder" +sidebarTitle: "SessionSettingsBuilder" +--- + +The `SessionSettingsBuilder` is a powerful configuration tool that allows you to customize every aspect of your call session before participants join. From controlling the initial audio/video state to customizing the UI layout and hiding specific controls, this builder gives you complete control over the call experience. + +Proper session configuration is crucial for creating a seamless user experience tailored to your application's specific needs. + + +These are pre-session configurations that must be set before joining a call. Once configured, pass the `SessionSettings` object to the `joinSession()` method. Settings cannot be changed after the session has started, though many features can be controlled dynamically during the call using call actions. + + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setTitle("Team Meeting") + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .setLayout(LayoutType.TILE) + .startAudioMuted(false) + .startVideoPaused(false) + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setTitle("Team Meeting") + .setDisplayName("John Doe") + .setType(SessionType.VIDEO) + .setLayout(LayoutType.TILE) + .startAudioMuted(false) + .startVideoPaused(false) + .build(); +``` + + + +## Session Settings + +### Title + +**Method:** `setTitle(String)` + +Sets the title that appears in the call header. This helps participants identify the purpose or name of the call session. The title is displayed prominently at the top of the call interface. + + + +```kotlin +.setTitle("Team Meeting") +``` + + +```java +.setTitle("Team Meeting") +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `title` | String | null | + +### Display Name + +**Method:** `setDisplayName(String)` + +Sets the display name that will be shown to other participants in the call. This name appears on your video tile and in the participant list, helping others identify you during the session. + + + +```kotlin +.setDisplayName("John Doe") +``` + + +```java +.setDisplayName("John Doe") +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `displayName` | String | null | + +### Session Type + +**Method:** `setType(SessionType)` + +Defines the type of call session. Choose `VIDEO` for video calls with camera enabled, or `AUDIO` for audio-only calls. This setting determines whether video streaming is enabled by default. + + + +```kotlin +.setType(SessionType.VIDEO) +``` + + +```java +.setType(SessionType.VIDEO) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `type` | SessionType | VIDEO | + + +| Value | Description | +|-------|-------------| +| `VIDEO` | Video call with camera enabled | +| `AUDIO` | Audio-only call | + + +### Layout Mode + +**Method:** `setLayout(LayoutType)` + +Sets the initial layout mode for displaying participants. `TILE` shows all participants in a grid, `SPOTLIGHT` focuses on the active speaker with others in a sidebar, and `SIDEBAR` displays the main speaker with participants in a side panel. + + + +```kotlin +.setLayout(LayoutType.TILE) +``` + + +```java +.setLayout(LayoutType.TILE) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `layout` | LayoutType | TILE | + + +| Value | Description | +|-------|-------------| +| `TILE` | Grid layout showing all participants equally | +| `SPOTLIGHT` | Focus on active speaker with others in sidebar | +| `SIDEBAR` | Main speaker with participants in a sidebar | + + +### Idle Timeout Period + +**Method:** `setIdleTimeoutPeriod(int)` + +Configures the timeout duration in seconds before automatically ending the session when you're the only participant. This prevents sessions from running indefinitely when others have left. + + + +```kotlin +.setIdleTimeoutPeriod(300) +``` + + +```java +.setIdleTimeoutPeriod(300) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `seconds` | int | 300 | + +### Start Audio Muted + +**Method:** `startAudioMuted(boolean)` + +Determines whether the microphone is muted when joining the session. Set to `true` to join with audio muted, requiring users to manually unmute. Useful for large meetings to prevent background noise. + + + +```kotlin +.startAudioMuted(true) +``` + + +```java +.startAudioMuted(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `muted` | boolean | false | + +### Start Video Paused + +**Method:** `startVideoPaused(boolean)` + +Controls whether the camera is turned off when joining the session. Set to `true` to join with video disabled, allowing users to enable it when ready. Helpful for privacy or bandwidth considerations. + + + +```kotlin +.startVideoPaused(true) +``` + + +```java +.startVideoPaused(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `paused` | boolean | false | + +### Audio Mode + +**Method:** `setAudioMode(AudioMode)` + +Sets the initial audio output device for the call. Options include `SPEAKER` for loudspeaker, `EARPIECE` for phone earpiece, `BLUETOOTH` for connected Bluetooth devices, or `HEADPHONES` for wired headphones. + + + +```kotlin +.setAudioMode(AudioMode.SPEAKER) +``` + + +```java +.setAudioMode(AudioMode.SPEAKER) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `audioMode` | AudioMode | SPEAKER | + + +| Value | Description | +|-------|-------------| +| `SPEAKER` | Device loudspeaker | +| `EARPIECE` | Phone earpiece | +| `BLUETOOTH` | Connected Bluetooth device | +| `HEADPHONES` | Wired headphones | + + +### Initial Camera Facing + +**Method:** `setInitialCameraFacing(CameraFacing)` + +Specifies which camera to use when starting the session. Choose `FRONT` for the front-facing camera (selfie mode) or `BACK` for the rear camera. Users can switch cameras during the call. + + + +```kotlin +.setInitialCameraFacing(CameraFacing.FRONT) +``` + + +```java +.setInitialCameraFacing(CameraFacing.FRONT) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `cameraFacing` | CameraFacing | FRONT | + + +| Value | Description | +|-------|-------------| +| `FRONT` | Front-facing camera | +| `BACK` | Rear camera | + + +### Low Bandwidth Mode + +**Method:** `enableLowBandwidthMode(boolean)` + +Enables optimization for poor network conditions. When enabled, the SDK reduces video quality and adjusts streaming parameters to maintain call stability on slow or unstable connections. + + + +```kotlin +.enableLowBandwidthMode(true) +``` + + +```java +.enableLowBandwidthMode(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `enabled` | boolean | false | + +### Auto Start Recording + +**Method:** `enableAutoStartRecording(boolean)` + +Automatically starts recording the session as soon as it begins. When enabled, recording starts without manual intervention, ensuring the entire session is captured from the start. + + + +```kotlin +.enableAutoStartRecording(true) +``` + + +```java +.enableAutoStartRecording(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `enabled` | boolean | false | + +### Hide Control Panel + +**Method:** `hideControlPanel(boolean)` + +Hides the bottom control bar that contains call action buttons. Set to `true` to remove the control panel entirely, useful for custom UI implementations or view-only modes. + + + +```kotlin +.hideControlPanel(true) +``` + + +```java +.hideControlPanel(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Header Panel + +**Method:** `hideHeaderPanel(boolean)` + +Hides the top header bar that displays the call title and session information. Set to `true` to maximize the video viewing area or implement a custom header. + + + +```kotlin +.hideHeaderPanel(true) +``` + + +```java +.hideHeaderPanel(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Session Timer + +**Method:** `hideSessionTimer(boolean)` + +Hides the session duration timer that shows how long the call has been active. Set to `true` to remove the timer display from the interface. + + + +```kotlin +.hideSessionTimer(true) +``` + + +```java +.hideSessionTimer(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Leave Session Button + +**Method:** `hideLeaveSessionButton(boolean)` + +Hides the button that allows users to leave or end the call. Set to `true` to remove this button, requiring an alternative method to exit the session. + + + +```kotlin +.hideLeaveSessionButton(true) +``` + + +```java +.hideLeaveSessionButton(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Toggle Audio Button + +**Method:** `hideToggleAudioButton(boolean)` + +Hides the microphone mute/unmute button from the control panel. Set to `true` to remove audio controls, useful when audio control should be managed programmatically. + + + +```kotlin +.hideToggleAudioButton(true) +``` + + +```java +.hideToggleAudioButton(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Toggle Video Button + +**Method:** `hideToggleVideoButton(boolean)` + +Hides the camera on/off button from the control panel. Set to `true` to remove video controls, useful for audio-only calls or custom video control implementations. + + + +```kotlin +.hideToggleVideoButton(true) +``` + + +```java +.hideToggleVideoButton(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Switch Camera Button + +**Method:** `hideSwitchCameraButton(boolean)` + +Hides the button that allows switching between front and rear cameras. Set to `true` to remove this control, useful for devices with single cameras or fixed camera requirements. + + + +```kotlin +.hideSwitchCameraButton(true) +``` + + +```java +.hideSwitchCameraButton(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Recording Button + +**Method:** `hideRecordingButton(boolean)` + +Hides the recording start/stop button from the control panel. Set to `false` to show the recording button, allowing users to manually control session recording. + + + +```kotlin +.hideRecordingButton(false) +``` + + +```java +.hideRecordingButton(false) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | true | + +### Hide Screen Sharing Button + +**Method:** `hideScreenSharingButton(boolean)` + +Hides the screen sharing button from the control panel. Set to `true` to remove screen sharing controls, useful when screen sharing is not needed or managed separately. + + + +```kotlin +.hideScreenSharingButton(true) +``` + + +```java +.hideScreenSharingButton(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Audio Mode Button + +**Method:** `hideAudioModeButton(boolean)` + +Hides the button that toggles between speaker, earpiece, and other audio output modes. Set to `true` to remove audio mode controls from the interface. + + + +```kotlin +.hideAudioModeButton(true) +``` + + +```java +.hideAudioModeButton(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Raise Hand Button + +**Method:** `hideRaiseHandButton(boolean)` + +Hides the raise hand button that participants use to signal they want to speak. Set to `true` to remove this feature from the interface. + + + +```kotlin +.hideRaiseHandButton(true) +``` + + +```java +.hideRaiseHandButton(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Share Invite Button + +**Method:** `hideShareInviteButton(boolean)` + +Hides the button that allows sharing session invite links with others. Set to `false` to show the invite button, enabling easy participant invitation. + + + +```kotlin +.hideShareInviteButton(false) +``` + + +```java +.hideShareInviteButton(false) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | true | + +### Hide Participant List Button + +**Method:** `hideParticipantListButton(boolean)` + +Hides the button that opens the participant list view. Set to `true` to remove access to the participant list, useful for simplified interfaces. + + + +```kotlin +.hideParticipantListButton(true) +``` + + +```java +.hideParticipantListButton(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Change Layout Button + +**Method:** `hideChangeLayoutButton(boolean)` + +Hides the button that allows switching between different layout modes (tile, spotlight, sidebar). Set to `true` to lock the layout to the initial setting. + + + +```kotlin +.hideChangeLayoutButton(true) +``` + + +```java +.hideChangeLayoutButton(true) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | false | + +### Hide Chat Button + +**Method:** `hideChatButton(boolean)` + +Hides the button that opens the in-call chat interface. Set to `false` to show the chat button, enabling text communication during calls. + + + +```kotlin +.hideChatButton(false) +``` + + +```java +.hideChatButton(false) +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hide` | boolean | true | + + +| Enum | Value | Description | +|------|-------|-------------| +| `SessionType` | `VIDEO` | Video call with camera enabled | +| | `AUDIO` | Audio-only call | +| `LayoutType` | `TILE` | Grid layout showing all participants equally | +| | `SPOTLIGHT` | Focus on active speaker with others in sidebar | +| | `SIDEBAR` | Main speaker with participants in a sidebar | +| `AudioMode` | `SPEAKER` | Device loudspeaker | +| | `EARPIECE` | Phone earpiece | +| | `BLUETOOTH` | Connected Bluetooth device | +| | `HEADPHONES` | Wired headphones | +| `CameraFacing` | `FRONT` | Front-facing camera | +| | `BACK` | Rear camera | + diff --git a/calls/android/voip-calling.mdx b/calls/android/voip-calling.mdx new file mode 100644 index 00000000..a43b83ef --- /dev/null +++ b/calls/android/voip-calling.mdx @@ -0,0 +1,2151 @@ +--- +title: "VoIP Calling" +sidebarTitle: "VoIP Calling" +--- + +Implement native VoIP calling that works when your app is in the background or killed. This guide shows how to integrate Android's Telecom framework with CometChat to display system call UI, handle calls from the lock screen, and provide a native calling experience. + +## Overview + +VoIP calling differs from [basic in-app ringing](/calls/android/ringing) by leveraging Android's `ConnectionService` to: +- Show incoming calls on lock screen with system UI +- Handle calls when app is in background or killed +- Integrate with Bluetooth, car systems, and wearables +- Provide consistent call experience across Android devices + +```mermaid +flowchart TB + subgraph "Incoming Call Flow" + A[FCM Push Notification] --> B[FirebaseMessagingService] + B --> C{App State?} + C -->|Background/Killed| D[ConnectionService] + C -->|Foreground| E[CometChat CallListener] + D --> F[System Call UI] + E --> G[In-App Call UI] + F --> H{User Action} + G --> H + H -->|Accept| I[CometChat.acceptCall] + H -->|Reject| J[CometChat.rejectCall] + I --> K[CometChatCalls.joinSession] + end +``` + +## Prerequisites + +Before implementing VoIP calling, ensure you have: + +- [CometChat Chat SDK](/sdk/android/overview) and [Calls SDK](/calls/android/setup) integrated +- [Firebase Cloud Messaging (FCM)](/notifications/android-push-notifications) configured +- [Push notifications enabled](/notifications/push-integration) in CometChat Dashboard +- Android 8.0+ (API 26+) for ConnectionService support + + +This documentation builds on the [Ringing](/calls/android/ringing) functionality. Make sure you understand basic call signaling before implementing VoIP. + + +--- + +## Architecture Overview + +The VoIP implementation consists of several components working together: + +```mermaid +flowchart LR + subgraph "Your App" + A[FirebaseMessagingService] + B[ConnectionService] + C[CallActivity] + D[CallNotificationManager] + end + + subgraph "CometChat" + E[Chat SDK] + F[Calls SDK] + end + + subgraph "Android System" + G[Telecom Manager] + H[System Call UI] + end + + A -->|Call Push| D + D -->|Background| B + B <-->|Register| G + G --> H + H -->|Accept/Reject| B + B -->|Accept| E + E -->|Session| F + F --> C +``` + +| Component | Purpose | +|-----------|---------| +| `FirebaseMessagingService` | Receives push notifications for incoming calls when app is in background | +| `ConnectionService` | Android Telecom framework integration - manages call state with the system | +| `CallNotificationManager` | Decides whether to show system call UI or fallback notification | +| `PhoneAccount` | Registers your app as a calling app with Android's Telecom system | +| `Connection` | Represents an individual call and handles user actions (accept/reject/hold) | + +--- + +## Step 1: Configure Push Notifications + +Push notifications are essential for receiving incoming calls when your app is not in the foreground. When a call is initiated, CometChat sends a push notification to the receiver's device. + + +For detailed FCM setup instructions, see the [Android Push Notifications](/notifications/android-push-notifications) documentation. + + +### 1.1 Add FCM Dependencies + +Add Firebase Messaging to your `build.gradle`: + +```groovy +dependencies { + implementation 'com.google.firebase:firebase-messaging:23.4.0' +} +``` + +### 1.2 Create FirebaseMessagingService + +This service receives push notifications from FCM. When a call notification arrives, it extracts the call data and decides how to display the incoming call based on the app's state. + + + + +```kotlin +class CallFirebaseMessagingService : FirebaseMessagingService() { + + override fun onMessageReceived(remoteMessage: RemoteMessage) { + val data = remoteMessage.data + + // Check if this is a call notification by examining the "type" field + // CometChat sends call notifications with type="call" + if (data["type"] == "call") { + handleIncomingCall(data) + } + } + + private fun handleIncomingCall(data: Map) { + // Extract call information from the push payload + // These fields are sent by CometChat when a call is initiated + val sessionId = data["sessionId"] ?: return + val callerName = data["senderName"] ?: "Unknown" + val callerUid = data["senderUid"] ?: return + val callType = data["callType"] ?: "video" // "audio" or "video" + val callerAvatar = data["senderAvatar"] + + // Create a CallData object to pass call information between components + val callData = CallData( + sessionId = sessionId, + callerName = callerName, + callerUid = callerUid, + callType = callType, + callerAvatar = callerAvatar + ) + + // If app is in foreground, let CometChat's CallListener handle it + // This provides a seamless experience with in-app UI + if (isAppInForeground()) { + return + } + + // App is in background/killed - show system call UI via ConnectionService + // This ensures the user sees the incoming call even when not using the app + CallNotificationManager.showIncomingCall(this, callData) + } + + /** + * Checks if the app is currently visible to the user. + * We only want to use ConnectionService when the app is in background. + */ + private fun isAppInForeground(): Boolean { + val activityManager = getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager + val appProcesses = activityManager.runningAppProcesses ?: return false + val packageName = packageName + for (appProcess in appProcesses) { + if (appProcess.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + && appProcess.processName == packageName) { + return true + } + } + return false + } + + /** + * Called when FCM generates a new token. + * Register this token with CometChat to receive push notifications. + */ + override fun onNewToken(token: String) { + // Register the FCM token with CometChat's push notification service + // This links the device to the logged-in user for push delivery + CometChat.registerTokenForPushNotification(token, object : CometChat.CallbackListener() { + override fun onSuccess(s: String) { + Log.d(TAG, "Push token registered successfully") + } + override fun onError(e: CometChatException) { + Log.e(TAG, "Token registration failed: ${e.message}") + } + }) + } + + companion object { + private const val TAG = "CallFCMService" + } +} +``` + + +```java +public class CallFirebaseMessagingService extends FirebaseMessagingService { + + private static final String TAG = "CallFCMService"; + + @Override + public void onMessageReceived(RemoteMessage remoteMessage) { + Map data = remoteMessage.getData(); + + // Check if this is a call notification by examining the "type" field + // CometChat sends call notifications with type="call" + if ("call".equals(data.get("type"))) { + handleIncomingCall(data); + } + } + + private void handleIncomingCall(Map data) { + // Extract call information from the push payload + // These fields are sent by CometChat when a call is initiated + String sessionId = data.get("sessionId"); + String callerName = data.get("senderName"); + String callerUid = data.get("senderUid"); + String callType = data.get("callType"); + String callerAvatar = data.get("senderAvatar"); + + if (sessionId == null || callerUid == null) return; + if (callerName == null) callerName = "Unknown"; + if (callType == null) callType = "video"; + + // Create a CallData object to pass call information between components + CallData callData = new CallData( + sessionId, callerName, callerUid, callType, callerAvatar + ); + + // If app is in foreground, let CometChat's CallListener handle it + if (isAppInForeground()) { + return; + } + + // App is in background/killed - show system call UI via ConnectionService + CallNotificationManager.showIncomingCall(this, callData); + } + + /** + * Checks if the app is currently visible to the user. + * We only want to use ConnectionService when the app is in background. + */ + private boolean isAppInForeground() { + ActivityManager activityManager = + (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE); + List appProcesses = + activityManager.getRunningAppProcesses(); + if (appProcesses == null) return false; + + String packageName = getPackageName(); + for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { + if (appProcess.importance == + ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND + && appProcess.processName.equals(packageName)) { + return true; + } + } + return false; + } + + /** + * Called when FCM generates a new token. + * Register this token with CometChat to receive push notifications. + */ + @Override + public void onNewToken(String token) { + // Register the FCM token with CometChat's push notification service + CometChat.registerTokenForPushNotification(token, new CometChat.CallbackListener() { + @Override + public void onSuccess(String s) { + Log.d(TAG, "Push token registered successfully"); + } + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Token registration failed: " + e.getMessage()); + } + }); + } +} +``` + + + +### 1.3 Create CallData Model + +The `CallData` class is a simple data container that holds all information about an incoming or outgoing call. It implements `Parcelable` so it can be passed between Android components (Activities, Services, BroadcastReceivers). + + + +```kotlin +/** + * Data class representing call information. + * Implements Parcelable to allow passing between Android components. + */ +data class CallData( + val sessionId: String, // Unique identifier for the call session + val callerName: String, // Display name of the caller + val callerUid: String, // CometChat UID of the caller + val callType: String, // "audio" or "video" + val callerAvatar: String? // URL to caller's avatar image (optional) +) : Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString() ?: "", + parcel.readString() ?: "", + parcel.readString() ?: "", + parcel.readString() ?: "", + parcel.readString() + ) + + override fun writeToParcel(parcel: Parcel, flags: Int) { + parcel.writeString(sessionId) + parcel.writeString(callerName) + parcel.writeString(callerUid) + parcel.writeString(callType) + parcel.writeString(callerAvatar) + } + + override fun describeContents(): Int = 0 + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): CallData = CallData(parcel) + override fun newArray(size: Int): Array = arrayOfNulls(size) + } +} +``` + + +```java +/** + * Data class representing call information. + * Implements Parcelable to allow passing between Android components. + */ +public class CallData implements Parcelable { + private final String sessionId; // Unique identifier for the call session + private final String callerName; // Display name of the caller + private final String callerUid; // CometChat UID of the caller + private final String callType; // "audio" or "video" + private final String callerAvatar; // URL to caller's avatar image (optional) + + public CallData(String sessionId, String callerName, String callerUid, + String callType, String callerAvatar) { + this.sessionId = sessionId; + this.callerName = callerName; + this.callerUid = callerUid; + this.callType = callType; + this.callerAvatar = callerAvatar; + } + + // Getters + public String getSessionId() { return sessionId; } + public String getCallerName() { return callerName; } + public String getCallerUid() { return callerUid; } + public String getCallType() { return callType; } + public String getCallerAvatar() { return callerAvatar; } + + // Parcelable implementation for passing between components + protected CallData(Parcel in) { + sessionId = in.readString(); + callerName = in.readString(); + callerUid = in.readString(); + callType = in.readString(); + callerAvatar = in.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeString(sessionId); + dest.writeString(callerName); + dest.writeString(callerUid); + dest.writeString(callType); + dest.writeString(callerAvatar); + } + + @Override + public int describeContents() { return 0; } + + public static final Creator CREATOR = new Creator() { + @Override + public CallData createFromParcel(Parcel in) { return new CallData(in); } + @Override + public CallData[] newArray(int size) { return new CallData[size]; } + }; +} +``` + + + +--- + +## Step 2: Register PhoneAccount + +A `PhoneAccount` tells Android that your app can handle phone calls. This registration is required for the system to route incoming calls to your app and display the native call UI. + +### 2.1 Create PhoneAccountManager + +This singleton class handles registering your app with Android's Telecom system. The `PhoneAccount` must be registered before you can receive or make VoIP calls. + + + +```kotlin +/** + * Manages PhoneAccount registration with Android's Telecom system. + * Must be called once when the app starts (typically in Application.onCreate). + */ +object PhoneAccountManager { + private const val PHONE_ACCOUNT_ID = "cometchat_voip_account" + private var phoneAccountHandle: PhoneAccountHandle? = null + + /** + * Registers your app as a calling app with Android's Telecom system. + * Call this in your Application.onCreate() method. + */ + fun register(context: Context) { + val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + + // ComponentName points to your ConnectionService implementation + val componentName = ComponentName(context, CallConnectionService::class.java) + + // PhoneAccountHandle uniquely identifies your calling account + phoneAccountHandle = PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID) + + // Build the PhoneAccount with required capabilities + val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "CometChat Calls") + .setCapabilities( + // CAPABILITY_CALL_PROVIDER: Can make and receive calls + // CAPABILITY_SELF_MANAGED: Manages its own call UI (required for VoIP) + PhoneAccount.CAPABILITY_CALL_PROVIDER or + PhoneAccount.CAPABILITY_SELF_MANAGED + ) + .build() + + // Register with the system + telecomManager.registerPhoneAccount(phoneAccount) + } + + /** + * Returns the PhoneAccountHandle for use with TelecomManager calls. + */ + fun getPhoneAccountHandle(context: Context): PhoneAccountHandle { + if (phoneAccountHandle == null) { + val componentName = ComponentName(context, CallConnectionService::class.java) + phoneAccountHandle = PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID) + } + return phoneAccountHandle!! + } + + /** + * Checks if the user has enabled the PhoneAccount in system settings. + * Some devices require manual enabling in Settings > Apps > Phone > Calling accounts. + */ + fun isPhoneAccountEnabled(context: Context): Boolean { + val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + val account = telecomManager.getPhoneAccount(getPhoneAccountHandle(context)) + return account?.isEnabled == true + } + + /** + * Opens system settings where user can enable the PhoneAccount. + * Call this if isPhoneAccountEnabled() returns false. + */ + fun openPhoneAccountSettings(context: Context) { + val intent = Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK + context.startActivity(intent) + } +} +``` + + +```java +/** + * Manages PhoneAccount registration with Android's Telecom system. + * Must be called once when the app starts (typically in Application.onCreate). + */ +public class PhoneAccountManager { + private static final String PHONE_ACCOUNT_ID = "cometchat_voip_account"; + private static PhoneAccountHandle phoneAccountHandle; + + /** + * Registers your app as a calling app with Android's Telecom system. + * Call this in your Application.onCreate() method. + */ + public static void register(Context context) { + TelecomManager telecomManager = + (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); + + // ComponentName points to your ConnectionService implementation + ComponentName componentName = + new ComponentName(context, CallConnectionService.class); + + // PhoneAccountHandle uniquely identifies your calling account + phoneAccountHandle = new PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID); + + // Build the PhoneAccount with required capabilities + PhoneAccount phoneAccount = PhoneAccount.builder(phoneAccountHandle, "CometChat Calls") + .setCapabilities( + // CAPABILITY_CALL_PROVIDER: Can make and receive calls + // CAPABILITY_SELF_MANAGED: Manages its own call UI (required for VoIP) + PhoneAccount.CAPABILITY_CALL_PROVIDER | + PhoneAccount.CAPABILITY_SELF_MANAGED + ) + .build(); + + // Register with the system + telecomManager.registerPhoneAccount(phoneAccount); + } + + /** + * Returns the PhoneAccountHandle for use with TelecomManager calls. + */ + public static PhoneAccountHandle getPhoneAccountHandle(Context context) { + if (phoneAccountHandle == null) { + ComponentName componentName = + new ComponentName(context, CallConnectionService.class); + phoneAccountHandle = new PhoneAccountHandle(componentName, PHONE_ACCOUNT_ID); + } + return phoneAccountHandle; + } + + /** + * Checks if the user has enabled the PhoneAccount in system settings. + */ + public static boolean isPhoneAccountEnabled(Context context) { + TelecomManager telecomManager = + (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); + PhoneAccount account = telecomManager.getPhoneAccount(getPhoneAccountHandle(context)); + return account != null && account.isEnabled(); + } + + /** + * Opens system settings where user can enable the PhoneAccount. + */ + public static void openPhoneAccountSettings(Context context) { + Intent intent = new Intent(TelecomManager.ACTION_CHANGE_PHONE_ACCOUNTS); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } +} +``` + + + +### 2.2 Register on App Start + +Register the PhoneAccount when your app starts. This should be done in your `Application` class to ensure it's registered before any calls can be received. + + + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + + // Initialize CometChat (see Setup guide) + // CometChat.init(...) + + // Register PhoneAccount for VoIP calling + // This must be done before receiving any calls + PhoneAccountManager.register(this) + } +} +``` + + +```java +public class MyApplication extends Application { + @Override + public void onCreate() { + super.onCreate(); + + // Initialize CometChat (see Setup guide) + // CometChat.init(...) + + // Register PhoneAccount for VoIP calling + // This must be done before receiving any calls + PhoneAccountManager.register(this); + } +} +``` + + + +--- + +## Step 3: Implement ConnectionService + +The `ConnectionService` is the core component that bridges your app with Android's Telecom framework. It creates `Connection` objects that represent individual calls and handle user interactions. + +### 3.1 Create CallConnectionService + +This service is called by Android when a new incoming or outgoing call needs to be created. It's responsible for creating `Connection` objects that manage the call state. + + + + +```kotlin +/** + * ConnectionService implementation for VoIP calling. + * Android's Telecom framework calls this service to create Connection objects + * for incoming and outgoing calls. + */ +class CallConnectionService : ConnectionService() { + + /** + * Called by Android when an incoming call is reported via TelecomManager.addNewIncomingCall(). + * Creates a Connection object that will handle the incoming call. + */ + override fun onCreateIncomingConnection( + connectionManagerPhoneAccount: PhoneAccountHandle?, + request: ConnectionRequest? + ): Connection { + // Extract call data from the request extras + val extras = request?.extras + val callData = extras?.getParcelable(EXTRA_CALL_DATA) + + // Create a new Connection to represent this call + val connection = CallConnection(applicationContext, callData) + + // Set initial state: initializing -> ringing + // This triggers the system to show the incoming call UI + connection.setInitializing() + connection.setRinging() + + // Store the connection so we can access it from other components + CallConnectionHolder.setConnection(connection) + + return connection + } + + /** + * Called by Android when an outgoing call is placed via TelecomManager.placeCall(). + * Creates a Connection object that will handle the outgoing call. + */ + override fun onCreateOutgoingConnection( + connectionManagerPhoneAccount: PhoneAccountHandle?, + request: ConnectionRequest? + ): Connection { + val extras = request?.extras + val callData = extras?.getParcelable(EXTRA_CALL_DATA) + + val connection = CallConnection(applicationContext, callData) + + // Set initial state: initializing -> dialing + // This triggers the system to show the outgoing call UI + connection.setInitializing() + connection.setDialing() + + CallConnectionHolder.setConnection(connection) + + return connection + } + + /** + * Called when the system fails to create an incoming connection. + * This can happen due to permission issues or system constraints. + */ + override fun onCreateIncomingConnectionFailed( + connectionManagerPhoneAccount: PhoneAccountHandle?, + request: ConnectionRequest? + ) { + Log.e(TAG, "Failed to create incoming connection") + // Consider showing a fallback notification here + } + + /** + * Called when the system fails to create an outgoing connection. + */ + override fun onCreateOutgoingConnectionFailed( + connectionManagerPhoneAccount: PhoneAccountHandle?, + request: ConnectionRequest? + ) { + Log.e(TAG, "Failed to create outgoing connection") + } + + companion object { + private const val TAG = "CallConnectionService" + const val EXTRA_CALL_DATA = "extra_call_data" + } +} +``` + + +```java +/** + * ConnectionService implementation for VoIP calling. + * Android's Telecom framework calls this service to create Connection objects + * for incoming and outgoing calls. + */ +public class CallConnectionService extends ConnectionService { + + private static final String TAG = "CallConnectionService"; + public static final String EXTRA_CALL_DATA = "extra_call_data"; + + /** + * Called by Android when an incoming call is reported via TelecomManager.addNewIncomingCall(). + * Creates a Connection object that will handle the incoming call. + */ + @Override + public Connection onCreateIncomingConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + + // Extract call data from the request extras + Bundle extras = request.getExtras(); + CallData callData = extras.getParcelable(EXTRA_CALL_DATA); + + // Create a new Connection to represent this call + CallConnection connection = new CallConnection(getApplicationContext(), callData); + + // Set initial state: initializing -> ringing + // This triggers the system to show the incoming call UI + connection.setInitializing(); + connection.setRinging(); + + // Store the connection so we can access it from other components + CallConnectionHolder.setConnection(connection); + + return connection; + } + + /** + * Called by Android when an outgoing call is placed via TelecomManager.placeCall(). + * Creates a Connection object that will handle the outgoing call. + */ + @Override + public Connection onCreateOutgoingConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + + Bundle extras = request.getExtras(); + CallData callData = extras.getParcelable(EXTRA_CALL_DATA); + + CallConnection connection = new CallConnection(getApplicationContext(), callData); + + // Set initial state: initializing -> dialing + connection.setInitializing(); + connection.setDialing(); + + CallConnectionHolder.setConnection(connection); + + return connection; + } + + @Override + public void onCreateIncomingConnectionFailed( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + Log.e(TAG, "Failed to create incoming connection"); + } + + @Override + public void onCreateOutgoingConnectionFailed( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + Log.e(TAG, "Failed to create outgoing connection"); + } +} +``` + + + +### 3.2 Create CallConnection + +The `Connection` class represents an individual call. It receives callbacks from Android when the user interacts with the call (answer, reject, hold, etc.) and is responsible for updating the call state and communicating with CometChat. + + + +```kotlin +/** + * Represents an individual VoIP call. + * Handles user actions (answer, reject, disconnect) and bridges to CometChat SDK. + */ +class CallConnection( + private val context: Context, + private val callData: CallData? +) : Connection() { + + init { + // PROPERTY_SELF_MANAGED: We manage our own call UI (not using system dialer) + connectionProperties = PROPERTY_SELF_MANAGED + + // Set capabilities for this call + // CAPABILITY_MUTE: User can mute the call + // CAPABILITY_SUPPORT_HOLD/CAPABILITY_HOLD: User can put call on hold + connectionCapabilities = CAPABILITY_MUTE or + CAPABILITY_SUPPORT_HOLD or + CAPABILITY_HOLD + + // Set caller information for the system call UI + callData?.let { + // Display name shown on incoming call screen + setCallerDisplayName(it.callerName, TelecomManager.PRESENTATION_ALLOWED) + // Address (used for call log and display) + setAddress( + Uri.parse("tel:${it.callerUid}"), + TelecomManager.PRESENTATION_ALLOWED + ) + } + + // Mark this as a VoIP call for proper audio routing + audioModeIsVoip = true + } + + /** + * Called when user taps "Answer" on the incoming call screen. + * Accept the call via CometChat and launch the call activity. + */ + override fun onAnswer() { + Log.d(TAG, "Call answered by user") + + // Update connection state to active (call is now connected) + setActive() + + callData?.let { data -> + // Accept the call via CometChat Chat SDK + // This notifies the caller that we've accepted + CometChat.acceptCall(data.sessionId, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "CometChat call accepted successfully") + // Launch the call activity to show the video/audio UI + launchCallActivity(data) + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed to accept call: ${e.message}") + // Call failed - disconnect and clean up + setDisconnected(DisconnectCause(DisconnectCause.ERROR)) + destroy() + } + }) + } + } + + /** + * Called when user taps "Decline" on the incoming call screen. + * Reject the call via CometChat. + */ + override fun onReject() { + Log.d(TAG, "Call rejected by user") + + callData?.let { data -> + // Reject the call via CometChat Chat SDK + // This notifies the caller that we've declined + CometChat.rejectCall( + data.sessionId, + CometChatConstants.CALL_STATUS_REJECTED, + object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "Call rejected successfully") + } + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed to reject call: ${e.message}") + } + } + ) + } + + // Update connection state and clean up + setDisconnected(DisconnectCause(DisconnectCause.REJECTED)) + destroy() + } + + /** + * Called when user ends the call (taps end call button). + * Leave the call session and notify the other participant. + */ + override fun onDisconnect() { + Log.d(TAG, "Call disconnected") + + // Leave the Calls SDK session if active + if (CallSession.getInstance().isSessionActive) { + CallSession.getInstance().leaveSession() + } + + // End the call via CometChat Chat SDK + // This notifies the other participant that the call has ended + callData?.let { data -> + CometChat.endCall(data.sessionId, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "Call ended successfully") + } + override fun onError(e: CometChatException) { + Log.e(TAG, "Failed to end call: ${e.message}") + } + }) + } + + setDisconnected(DisconnectCause(DisconnectCause.LOCAL)) + destroy() + } + + /** + * Called when user puts the call on hold. + */ + override fun onHold() { + setOnHold() + // Mute audio when on hold + CallSession.getInstance().muteAudio() + } + + /** + * Called when user takes the call off hold. + */ + override fun onUnhold() { + setActive() + CallSession.getInstance().unMuteAudio() + } + + /** + * Launches the CallActivity to show the call UI. + */ + private fun launchCallActivity(callData: CallData) { + val intent = Intent(context, CallActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(CallActivity.EXTRA_SESSION_ID, callData.sessionId) + putExtra(CallActivity.EXTRA_CALL_TYPE, callData.callType) + putExtra(CallActivity.EXTRA_IS_INCOMING, true) + } + context.startActivity(intent) + } + + /** + * Public method to end the call from outside this class. + */ + fun endCall() { + onDisconnect() + } + + companion object { + private const val TAG = "CallConnection" + } +} +``` + + +```java +/** + * Represents an individual VoIP call. + * Handles user actions (answer, reject, disconnect) and bridges to CometChat SDK. + */ +public class CallConnection extends Connection { + + private static final String TAG = "CallConnection"; + private final Context context; + private final CallData callData; + + public CallConnection(Context context, CallData callData) { + this.context = context; + this.callData = callData; + + // PROPERTY_SELF_MANAGED: We manage our own call UI + setConnectionProperties(PROPERTY_SELF_MANAGED); + + // Set capabilities for this call + setConnectionCapabilities( + CAPABILITY_MUTE | CAPABILITY_SUPPORT_HOLD | CAPABILITY_HOLD + ); + + // Set caller information for the system call UI + if (callData != null) { + setCallerDisplayName(callData.getCallerName(), TelecomManager.PRESENTATION_ALLOWED); + setAddress( + Uri.parse("tel:" + callData.getCallerUid()), + TelecomManager.PRESENTATION_ALLOWED + ); + } + + // Mark this as a VoIP call for proper audio routing + setAudioModeIsVoip(true); + } + + /** + * Called when user taps "Answer" on the incoming call screen. + */ + @Override + public void onAnswer() { + Log.d(TAG, "Call answered by user"); + setActive(); + + if (callData != null) { + // Accept the call via CometChat Chat SDK + CometChat.acceptCall(callData.getSessionId(), new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "CometChat call accepted successfully"); + launchCallActivity(callData); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to accept call: " + e.getMessage()); + setDisconnected(new DisconnectCause(DisconnectCause.ERROR)); + destroy(); + } + }); + } + } + + /** + * Called when user taps "Decline" on the incoming call screen. + */ + @Override + public void onReject() { + Log.d(TAG, "Call rejected by user"); + + if (callData != null) { + CometChat.rejectCall( + callData.getSessionId(), + CometChatConstants.CALL_STATUS_REJECTED, + new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "Call rejected successfully"); + } + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to reject call: " + e.getMessage()); + } + } + ); + } + + setDisconnected(new DisconnectCause(DisconnectCause.REJECTED)); + destroy(); + } + + /** + * Called when user ends the call. + */ + @Override + public void onDisconnect() { + Log.d(TAG, "Call disconnected"); + + if (CallSession.getInstance().isSessionActive()) { + CallSession.getInstance().leaveSession(); + } + + if (callData != null) { + CometChat.endCall(callData.getSessionId(), new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "Call ended successfully"); + } + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to end call: " + e.getMessage()); + } + }); + } + + setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + destroy(); + } + + @Override + public void onHold() { + setOnHold(); + CallSession.getInstance().muteAudio(); + } + + @Override + public void onUnhold() { + setActive(); + CallSession.getInstance().unMuteAudio(); + } + + private void launchCallActivity(CallData callData) { + Intent intent = new Intent(context, CallActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(CallActivity.EXTRA_SESSION_ID, callData.getSessionId()); + intent.putExtra(CallActivity.EXTRA_CALL_TYPE, callData.getCallType()); + intent.putExtra(CallActivity.EXTRA_IS_INCOMING, true); + context.startActivity(intent); + } + + public void endCall() { + onDisconnect(); + } +} +``` + + + +### 3.3 Create CallConnectionHolder + +This singleton holds a reference to the active `Connection` so it can be accessed from other components (like the CallActivity or BroadcastReceiver). + + + +```kotlin +/** + * Singleton to hold the active CallConnection. + * Allows other components to access and control the current call. + */ +object CallConnectionHolder { + private var connection: CallConnection? = null + + fun setConnection(conn: CallConnection?) { + connection = conn + } + + fun getConnection(): CallConnection? = connection + + /** + * Ends the current call and clears the reference. + */ + fun endCall() { + connection?.endCall() + connection = null + } + + fun hasActiveConnection(): Boolean = connection != null +} +``` + + +```java +/** + * Singleton to hold the active CallConnection. + * Allows other components to access and control the current call. + */ +public class CallConnectionHolder { + private static CallConnection connection; + + public static void setConnection(CallConnection conn) { + connection = conn; + } + + public static CallConnection getConnection() { + return connection; + } + + public static void endCall() { + if (connection != null) { + connection.endCall(); + connection = null; + } + } + + public static boolean hasActiveConnection() { + return connection != null; + } +} +``` + + + +--- + +## Step 4: Create CallNotificationManager + +This class is responsible for showing incoming calls to the user. It first tries to use the system call UI via `TelecomManager`, and falls back to a high-priority notification if that fails. + + + + +```kotlin +/** + * Manages showing incoming calls via Android's Telecom system. + * Falls back to a high-priority notification if Telecom fails. + */ +object CallNotificationManager { + + /** + * Shows an incoming call to the user. + * Tries to use the system call UI first, falls back to notification. + */ + fun showIncomingCall(context: Context, callData: CallData) { + val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + + // Prepare extras with call data for the ConnectionService + val extras = Bundle().apply { + putParcelable(CallConnectionService.EXTRA_CALL_DATA, callData) + putParcelable( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, + PhoneAccountManager.getPhoneAccountHandle(context) + ) + } + + try { + // Tell Android there's an incoming call + // This triggers onCreateIncomingConnection in our ConnectionService + telecomManager.addNewIncomingCall( + PhoneAccountManager.getPhoneAccountHandle(context), + extras + ) + } catch (e: SecurityException) { + // Permission denied - PhoneAccount may not be enabled + Log.e(TAG, "Permission denied for incoming call: ${e.message}") + showFallbackNotification(context, callData) + } catch (e: Exception) { + Log.e(TAG, "Failed to show incoming call: ${e.message}") + showFallbackNotification(context, callData) + } + } + + /** + * Places an outgoing call via the Telecom system. + */ + fun placeOutgoingCall(context: Context, callData: CallData) { + val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager + + val extras = Bundle().apply { + putParcelable(CallConnectionService.EXTRA_CALL_DATA, callData) + putParcelable( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, + PhoneAccountManager.getPhoneAccountHandle(context) + ) + } + + try { + telecomManager.placeCall( + Uri.parse("tel:${callData.callerUid}"), + extras + ) + } catch (e: SecurityException) { + Log.e(TAG, "Permission denied for outgoing call: ${e.message}") + } + } + + /** + * Shows a high-priority notification as fallback when Telecom fails. + * This notification has full-screen intent to show on lock screen. + */ + private fun showFallbackNotification(context: Context, callData: CallData) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) + as NotificationManager + + // Create notification channel (required for Android 8.0+) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val channel = NotificationChannel( + CHANNEL_ID, + "Incoming Calls", + NotificationManager.IMPORTANCE_HIGH // High importance for call notifications + ).apply { + description = "Notifications for incoming calls" + setSound(null, null) // We'll play our own ringtone + enableVibration(true) + } + notificationManager.createNotificationChannel(channel) + } + + // Create accept action - launches call when tapped + val acceptIntent = Intent(context, CallActionReceiver::class.java).apply { + action = ACTION_ACCEPT_CALL + putExtra(EXTRA_CALL_DATA, callData) + } + val acceptPendingIntent = PendingIntent.getBroadcast( + context, 0, acceptIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + // Create reject action + val rejectIntent = Intent(context, CallActionReceiver::class.java).apply { + action = ACTION_REJECT_CALL + putExtra(EXTRA_CALL_DATA, callData) + } + val rejectPendingIntent = PendingIntent.getBroadcast( + context, 1, rejectIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + // Full screen intent - shows activity on lock screen + val fullScreenIntent = Intent(context, IncomingCallActivity::class.java).apply { + putExtra(EXTRA_CALL_DATA, callData) + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + val fullScreenPendingIntent = PendingIntent.getActivity( + context, 2, fullScreenIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + + // Build the notification + val notification = NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_call) + .setContentTitle("Incoming ${callData.callType} call") + .setContentText("${callData.callerName} is calling...") + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_CALL) // Marks as call notification + .setAutoCancel(true) + .setOngoing(true) // Can't be swiped away + .setFullScreenIntent(fullScreenPendingIntent, true) // Shows on lock screen + .addAction(R.drawable.ic_call_end, "Decline", rejectPendingIntent) + .addAction(R.drawable.ic_call_accept, "Accept", acceptPendingIntent) + .build() + + notificationManager.notify(NOTIFICATION_ID, notification) + } + + /** + * Cancels the incoming call notification. + * Call this when the call is answered, rejected, or cancelled. + */ + fun cancelNotification(context: Context) { + val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) + as NotificationManager + notificationManager.cancel(NOTIFICATION_ID) + } + + private const val TAG = "CallNotificationManager" + private const val CHANNEL_ID = "incoming_calls" + private const val NOTIFICATION_ID = 1001 + const val ACTION_ACCEPT_CALL = "action_accept_call" + const val ACTION_REJECT_CALL = "action_reject_call" + const val EXTRA_CALL_DATA = "extra_call_data" +} +``` + + +```java +/** + * Manages showing incoming calls via Android's Telecom system. + * Falls back to a high-priority notification if Telecom fails. + */ +public class CallNotificationManager { + + private static final String TAG = "CallNotificationManager"; + private static final String CHANNEL_ID = "incoming_calls"; + private static final int NOTIFICATION_ID = 1001; + public static final String ACTION_ACCEPT_CALL = "action_accept_call"; + public static final String ACTION_REJECT_CALL = "action_reject_call"; + public static final String EXTRA_CALL_DATA = "extra_call_data"; + + /** + * Shows an incoming call to the user. + */ + public static void showIncomingCall(Context context, CallData callData) { + TelecomManager telecomManager = + (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); + + Bundle extras = new Bundle(); + extras.putParcelable(CallConnectionService.EXTRA_CALL_DATA, callData); + extras.putParcelable( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, + PhoneAccountManager.getPhoneAccountHandle(context) + ); + + try { + telecomManager.addNewIncomingCall( + PhoneAccountManager.getPhoneAccountHandle(context), + extras + ); + } catch (SecurityException e) { + Log.e(TAG, "Permission denied: " + e.getMessage()); + showFallbackNotification(context, callData); + } catch (Exception e) { + Log.e(TAG, "Failed to show incoming call: " + e.getMessage()); + showFallbackNotification(context, callData); + } + } + + /** + * Places an outgoing call via the Telecom system. + */ + public static void placeOutgoingCall(Context context, CallData callData) { + TelecomManager telecomManager = + (TelecomManager) context.getSystemService(Context.TELECOM_SERVICE); + + Bundle extras = new Bundle(); + extras.putParcelable(CallConnectionService.EXTRA_CALL_DATA, callData); + extras.putParcelable( + TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, + PhoneAccountManager.getPhoneAccountHandle(context) + ); + + try { + telecomManager.placeCall( + Uri.parse("tel:" + callData.getCallerUid()), + extras + ); + } catch (SecurityException e) { + Log.e(TAG, "Permission denied: " + e.getMessage()); + } + } + + /** + * Shows a high-priority notification as fallback. + */ + private static void showFallbackNotification(Context context, CallData callData) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + + // Create channel for Android 8.0+ + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + NotificationChannel channel = new NotificationChannel( + CHANNEL_ID, "Incoming Calls", NotificationManager.IMPORTANCE_HIGH + ); + channel.setDescription("Notifications for incoming calls"); + channel.setSound(null, null); + channel.enableVibration(true); + notificationManager.createNotificationChannel(channel); + } + + // Accept intent + Intent acceptIntent = new Intent(context, CallActionReceiver.class); + acceptIntent.setAction(ACTION_ACCEPT_CALL); + acceptIntent.putExtra(EXTRA_CALL_DATA, callData); + PendingIntent acceptPendingIntent = PendingIntent.getBroadcast( + context, 0, acceptIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + // Reject intent + Intent rejectIntent = new Intent(context, CallActionReceiver.class); + rejectIntent.setAction(ACTION_REJECT_CALL); + rejectIntent.putExtra(EXTRA_CALL_DATA, callData); + PendingIntent rejectPendingIntent = PendingIntent.getBroadcast( + context, 1, rejectIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + // Full screen intent for lock screen + Intent fullScreenIntent = new Intent(context, IncomingCallActivity.class); + fullScreenIntent.putExtra(EXTRA_CALL_DATA, callData); + fullScreenIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + PendingIntent fullScreenPendingIntent = PendingIntent.getActivity( + context, 2, fullScreenIntent, + PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE + ); + + Notification notification = new NotificationCompat.Builder(context, CHANNEL_ID) + .setSmallIcon(R.drawable.ic_call) + .setContentTitle("Incoming " + callData.getCallType() + " call") + .setContentText(callData.getCallerName() + " is calling...") + .setPriority(NotificationCompat.PRIORITY_MAX) + .setCategory(NotificationCompat.CATEGORY_CALL) + .setAutoCancel(true) + .setOngoing(true) + .setFullScreenIntent(fullScreenPendingIntent, true) + .addAction(R.drawable.ic_call_end, "Decline", rejectPendingIntent) + .addAction(R.drawable.ic_call_accept, "Accept", acceptPendingIntent) + .build(); + + notificationManager.notify(NOTIFICATION_ID, notification); + } + + public static void cancelNotification(Context context) { + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.cancel(NOTIFICATION_ID); + } +} +``` + + + +--- + +## Step 5: Create CallActionReceiver + +This `BroadcastReceiver` handles the Accept and Decline button taps from the fallback notification. + + + +```kotlin +/** + * Handles notification button actions (Accept/Decline). + * Used when the fallback notification is shown instead of system call UI. + */ +class CallActionReceiver : BroadcastReceiver() { + + override fun onReceive(context: Context, intent: Intent) { + val callData = intent.getParcelableExtra( + CallNotificationManager.EXTRA_CALL_DATA + ) ?: return + + when (intent.action) { + CallNotificationManager.ACTION_ACCEPT_CALL -> { + acceptCall(context, callData) + } + CallNotificationManager.ACTION_REJECT_CALL -> { + rejectCall(context, callData) + } + } + + // Always cancel the notification after handling + CallNotificationManager.cancelNotification(context) + } + + private fun acceptCall(context: Context, callData: CallData) { + // If we have an active Connection, use it + CallConnectionHolder.getConnection()?.onAnswer() + ?: run { + // No Connection - accept directly via CometChat + CometChat.acceptCall(callData.sessionId, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + // Launch call activity + val intent = Intent(context, CallActivity::class.java).apply { + flags = Intent.FLAG_ACTIVITY_NEW_TASK + putExtra(CallActivity.EXTRA_SESSION_ID, callData.sessionId) + putExtra(CallActivity.EXTRA_CALL_TYPE, callData.callType) + } + context.startActivity(intent) + } + override fun onError(e: CometChatException) { + Log.e(TAG, "Accept failed: ${e.message}") + } + }) + } + } + + private fun rejectCall(context: Context, callData: CallData) { + CallConnectionHolder.getConnection()?.onReject() + ?: run { + CometChat.rejectCall( + callData.sessionId, + CometChatConstants.CALL_STATUS_REJECTED, + object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "Call rejected") + } + override fun onError(e: CometChatException) { + Log.e(TAG, "Reject failed: ${e.message}") + } + } + ) + } + } + + companion object { + private const val TAG = "CallActionReceiver" + } +} +``` + + +```java +/** + * Handles notification button actions (Accept/Decline). + */ +public class CallActionReceiver extends BroadcastReceiver { + + private static final String TAG = "CallActionReceiver"; + + @Override + public void onReceive(Context context, Intent intent) { + CallData callData = intent.getParcelableExtra(CallNotificationManager.EXTRA_CALL_DATA); + if (callData == null) return; + + String action = intent.getAction(); + if (CallNotificationManager.ACTION_ACCEPT_CALL.equals(action)) { + acceptCall(context, callData); + } else if (CallNotificationManager.ACTION_REJECT_CALL.equals(action)) { + rejectCall(context, callData); + } + + CallNotificationManager.cancelNotification(context); + } + + private void acceptCall(Context context, CallData callData) { + CallConnection connection = CallConnectionHolder.getConnection(); + if (connection != null) { + connection.onAnswer(); + } else { + CometChat.acceptCall(callData.getSessionId(), new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Intent intent = new Intent(context, CallActivity.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(CallActivity.EXTRA_SESSION_ID, callData.getSessionId()); + intent.putExtra(CallActivity.EXTRA_CALL_TYPE, callData.getCallType()); + context.startActivity(intent); + } + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Accept failed: " + e.getMessage()); + } + }); + } + } + + private void rejectCall(Context context, CallData callData) { + CallConnection connection = CallConnectionHolder.getConnection(); + if (connection != null) { + connection.onReject(); + } else { + CometChat.rejectCall( + callData.getSessionId(), + CometChatConstants.CALL_STATUS_REJECTED, + new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "Call rejected"); + } + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Reject failed: " + e.getMessage()); + } + } + ); + } + } +} +``` + + + +--- + +## Step 6: Create CallActivity + +The `CallActivity` hosts the actual call UI using CometChat's Calls SDK. It joins the call session and handles the call lifecycle. + + + + +```kotlin +/** + * Activity that hosts the call UI. + * Joins the CometChat call session and displays the video/audio interface. + */ +class CallActivity : AppCompatActivity() { + + private lateinit var callSession: CallSession + private var sessionId: String? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + // Keep screen on during the call + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + callSession = CallSession.getInstance() + + // Get call parameters from intent + sessionId = intent.getStringExtra(EXTRA_SESSION_ID) + val callType = intent.getStringExtra(EXTRA_CALL_TYPE) ?: "video" + + // Join the call session + sessionId?.let { joinCallSession(it, callType) } + + // Listen for session end events + setupSessionStatusListener() + } + + /** + * Joins the CometChat call session. + * This connects to the actual audio/video call. + */ + private fun joinCallSession(sessionId: String, callType: String) { + val callContainer = findViewById(R.id.callContainer) + + // Configure session settings + // See SessionSettingsBuilder for all options + val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setType(if (callType == "video") SessionType.VIDEO else SessionType.AUDIO) + .build() + + // Join the session + CometChatCalls.joinSession( + sessionId = sessionId, + sessionSettings = sessionSettings, + view = callContainer, + context = this, + listener = object : CometChatCalls.CallbackListener() { + override fun onSuccess(p0: Void?) { + Log.d(TAG, "Joined call session successfully") + } + + override fun onError(exception: CometChatException) { + Log.e(TAG, "Failed to join call: ${exception.message}") + endCallAndFinish() + } + } + ) + } + + /** + * Listens for session status changes. + * Ends the activity when the call ends. + */ + private fun setupSessionStatusListener() { + callSession.addSessionStatusListener(this, object : SessionStatusListener() { + override fun onSessionLeft() { + runOnUiThread { endCallAndFinish() } + } + + override fun onConnectionClosed() { + runOnUiThread { endCallAndFinish() } + } + }) + } + + /** + * Properly ends the call and finishes the activity. + */ + private fun endCallAndFinish() { + // End the Connection (updates system call state) + CallConnectionHolder.endCall() + + // Notify other participant via CometChat + sessionId?.let { sid -> + CometChat.endCall(sid, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "Call ended successfully") + } + override fun onError(e: CometChatException) { + Log.e(TAG, "End call error: ${e.message}") + } + }) + } + + finish() + } + + override fun onBackPressed() { + // Prevent accidental back press during call + // User must use the end call button + } + + override fun onDestroy() { + super.onDestroy() + // Clean up if activity is destroyed while call is active + if (callSession.isSessionActive) { + callSession.leaveSession() + } + } + + companion object { + private const val TAG = "CallActivity" + const val EXTRA_SESSION_ID = "extra_session_id" + const val EXTRA_CALL_TYPE = "extra_call_type" + const val EXTRA_IS_INCOMING = "extra_is_incoming" + } +} +``` + + +```java +/** + * Activity that hosts the call UI. + */ +public class CallActivity extends AppCompatActivity { + + private static final String TAG = "CallActivity"; + public static final String EXTRA_SESSION_ID = "extra_session_id"; + public static final String EXTRA_CALL_TYPE = "extra_call_type"; + public static final String EXTRA_IS_INCOMING = "extra_is_incoming"; + + private CallSession callSession; + private String sessionId; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + // Keep screen on during the call + getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON); + + callSession = CallSession.getInstance(); + + sessionId = getIntent().getStringExtra(EXTRA_SESSION_ID); + String callType = getIntent().getStringExtra(EXTRA_CALL_TYPE); + if (callType == null) callType = "video"; + + if (sessionId != null) { + joinCallSession(sessionId, callType); + } + + setupSessionStatusListener(); + } + + private void joinCallSession(String sessionId, String callType) { + FrameLayout callContainer = findViewById(R.id.callContainer); + + SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setType(callType.equals("video") ? SessionType.VIDEO : SessionType.AUDIO) + .build(); + + CometChatCalls.joinSession( + sessionId, + sessionSettings, + callContainer, + this, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(Void unused) { + Log.d(TAG, "Joined call session successfully"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Failed to join call: " + e.getMessage()); + endCallAndFinish(); + } + } + ); + } + + private void setupSessionStatusListener() { + callSession.addSessionStatusListener(this, new SessionStatusListener() { + @Override + public void onSessionLeft() { + runOnUiThread(() -> endCallAndFinish()); + } + + @Override + public void onConnectionClosed() { + runOnUiThread(() -> endCallAndFinish()); + } + }); + } + + private void endCallAndFinish() { + CallConnectionHolder.endCall(); + + if (sessionId != null) { + CometChat.endCall(sessionId, new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "Call ended successfully"); + } + @Override + public void onError(CometChatException e) { + Log.e(TAG, "End call error: " + e.getMessage()); + } + }); + } + + finish(); + } + + @Override + public void onBackPressed() { + // Prevent accidental back press + } + + @Override + protected void onDestroy() { + super.onDestroy(); + if (callSession.isSessionActive()) { + callSession.leaveSession(); + } + } +} +``` + + + + +```xml + + +``` + + +--- + +## Step 7: Configure AndroidManifest + +Add all required permissions and component declarations to your `AndroidManifest.xml`: + +```xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +``` + +--- + +## Step 8: Initiate Outgoing Calls + +To make an outgoing VoIP call, use the [CometChat Chat SDK](/sdk/android/calling) to initiate the call, then join the session: + + + +```kotlin +/** + * Initiates a VoIP call to another user. + * + * @param receiverId The CometChat UID of the user to call + * @param receiverName Display name of the receiver (for UI) + * @param callType "audio" or "video" + */ +fun initiateVoIPCall(receiverId: String, receiverName: String, callType: String) { + // Create a Call object with receiver info + val call = Call(receiverId, CometChatConstants.RECEIVER_TYPE_USER, callType) + + // Initiate the call via CometChat Chat SDK + // This sends a call notification to the receiver + CometChat.initiateCall(call, object : CometChat.CallbackListener() { + override fun onSuccess(call: Call) { + Log.d(TAG, "Call initiated with sessionId: ${call.sessionId}") + + // Create CallData for the outgoing call + val callData = CallData( + sessionId = call.sessionId, + callerName = receiverName, + callerUid = receiverId, + callType = callType, + callerAvatar = null + ) + + // Option 1: Use Telecom system (shows system outgoing call UI) + // CallNotificationManager.placeOutgoingCall(this@MainActivity, callData) + + // Option 2: Launch CallActivity directly (recommended) + val intent = Intent(this@MainActivity, CallActivity::class.java).apply { + putExtra(CallActivity.EXTRA_SESSION_ID, call.sessionId) + putExtra(CallActivity.EXTRA_CALL_TYPE, callType) + putExtra(CallActivity.EXTRA_IS_INCOMING, false) + } + startActivity(intent) + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Call initiation failed: ${e.message}") + Toast.makeText(this@MainActivity, "Failed to start call", Toast.LENGTH_SHORT).show() + } + }) +} + +// Usage example: +// initiateVoIPCall("user123", "John Doe", CometChatConstants.CALL_TYPE_VIDEO) +``` + + +```java +/** + * Initiates a VoIP call to another user. + */ +void initiateVoIPCall(String receiverId, String receiverName, String callType) { + // Create a Call object with receiver info + Call call = new Call(receiverId, CometChatConstants.RECEIVER_TYPE_USER, callType); + + // Initiate the call via CometChat Chat SDK + CometChat.initiateCall(call, new CometChat.CallbackListener() { + @Override + public void onSuccess(Call call) { + Log.d(TAG, "Call initiated with sessionId: " + call.getSessionId()); + + CallData callData = new CallData( + call.getSessionId(), + receiverName, + receiverId, + callType, + null + ); + + // Launch CallActivity directly + Intent intent = new Intent(MainActivity.this, CallActivity.class); + intent.putExtra(CallActivity.EXTRA_SESSION_ID, call.getSessionId()); + intent.putExtra(CallActivity.EXTRA_CALL_TYPE, callType); + intent.putExtra(CallActivity.EXTRA_IS_INCOMING, false); + startActivity(intent); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Call initiation failed: " + e.getMessage()); + Toast.makeText(MainActivity.this, "Failed to start call", Toast.LENGTH_SHORT).show(); + } + }); +} +``` + + + +--- + +## Complete Flow Diagram + +This diagram shows the complete flow for both incoming and outgoing VoIP calls: + +```mermaid +sequenceDiagram + participant Caller + participant CallerApp + participant CometChat + participant FCM + participant ReceiverApp + participant AndroidTelecom + participant Receiver + + Note over Caller,Receiver: Outgoing Call Flow + Caller->>CallerApp: Tap call button + CallerApp->>CometChat: initiateCall() + CometChat-->>CallerApp: onSuccess(sessionId) + CallerApp->>CallerApp: Launch CallActivity + CallerApp->>CometChat: joinSession() + CometChat->>FCM: Send push notification + + Note over Caller,Receiver: Incoming Call Flow + FCM->>ReceiverApp: onMessageReceived() + ReceiverApp->>ReceiverApp: Parse call data + + alt App in Background + ReceiverApp->>AndroidTelecom: addNewIncomingCall() + AndroidTelecom->>Receiver: Show system call UI + Receiver->>AndroidTelecom: Accept/Reject + AndroidTelecom->>ReceiverApp: onAnswer()/onReject() + else App in Foreground + ReceiverApp->>Receiver: Show in-app call UI + Receiver->>ReceiverApp: Accept/Reject + end + + alt Call Accepted + ReceiverApp->>CometChat: acceptCall() + CometChat-->>ReceiverApp: onSuccess + ReceiverApp->>ReceiverApp: Launch CallActivity + ReceiverApp->>CometChat: joinSession() + CometChat-->>CallerApp: onOutgoingCallAccepted + Note over CallerApp,ReceiverApp: Call Connected + else Call Rejected + ReceiverApp->>CometChat: rejectCall() + CometChat-->>CallerApp: onOutgoingCallRejected + end +``` + +--- + +## Troubleshooting + +| Issue | Solution | +|-------|----------| +| System call UI not showing | Ensure PhoneAccount is registered and enabled. Check Settings > Apps > Phone > Calling accounts | +| Calls not received in background | Verify [FCM configuration](/notifications/android-push-notifications) and ensure high-priority notifications are enabled | +| Permission denied errors | Request `MANAGE_OWN_CALLS` permission at runtime on Android 11+ | +| Call drops immediately | Verify [CometChat authentication](/calls/android/authentication) is valid before joining session | +| Audio routing issues | Ensure `audioModeIsVoip = true` is set on the Connection | +| PhoneAccount not enabled | Call `PhoneAccountManager.openPhoneAccountSettings()` to let user enable it | + + +```kotlin +private val requiredPermissions = arrayOf( + Manifest.permission.CAMERA, + Manifest.permission.RECORD_AUDIO, + Manifest.permission.BLUETOOTH_CONNECT, // Android 12+ + Manifest.permission.POST_NOTIFICATIONS // Android 13+ +) + +private fun checkAndRequestPermissions() { + val permissionsToRequest = requiredPermissions.filter { + ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED + } + + if (permissionsToRequest.isNotEmpty()) { + ActivityCompat.requestPermissions( + this, + permissionsToRequest.toTypedArray(), + PERMISSION_REQUEST_CODE + ) + } +} + +override fun onRequestPermissionsResult( + requestCode: Int, + permissions: Array, + grantResults: IntArray +) { + super.onRequestPermissionsResult(requestCode, permissions, grantResults) + if (requestCode == PERMISSION_REQUEST_CODE) { + val allGranted = grantResults.all { it == PackageManager.PERMISSION_GRANTED } + if (!allGranted) { + // Show explanation or disable calling features + Toast.makeText(this, "Permissions required for calling", Toast.LENGTH_LONG).show() + } + } +} + +companion object { + private const val PERMISSION_REQUEST_CODE = 100 +} +``` + + +## Related Documentation + +- [Ringing](/calls/android/ringing) - Basic in-app call signaling +- [Actions](/calls/android/actions) - Control call functionality +- [Events](/calls/android/events) - Listen for call state changes +- [Push Notifications](/notifications/android-push-notifications) - FCM setup +- [Push Integration](/notifications/push-integration) - Register push tokens with CometChat diff --git a/docs.json b/docs.json index 5d85f63d..06b71ac0 100644 --- a/docs.json +++ b/docs.json @@ -4840,13 +4840,14 @@ "group": "Getting Started", "pages": [ "calls/android/setup", - "calls/android/authentication", - "calls/android/join-session" + "calls/android/authentication" ] }, { "group": "Call Session", "pages": [ + "calls/android/session-settings", + "calls/android/join-session", "calls/android/actions", "calls/android/events" ] @@ -4865,6 +4866,15 @@ "calls/android/raise-hand", "calls/android/idle-timeout" ] + }, + { + "group": "Advanced", + "pages": [ + "calls/android/custom-control-panel", + "calls/android/custom-participant-list", + "calls/android/voip-calling", + "calls/android/background-handling" + ] } ] }, From 2fd710bc0746bab79da3375806670b41d8671a44 Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Wed, 14 Jan 2026 19:43:17 +0530 Subject: [PATCH 04/10] docs(calls/api): enhance Calls API documentation - Rewrite overview with base URL, authentication, use cases, and complete object schemas - Add example response, pagination details, and error handling section - Enhance list-calls with common use cases, filtering tips, and example request - Enhance get-call with usage scenarios, response details, and participant states --- calls/api/get-call.mdx | 43 ++++++ calls/api/list-calls.mdx | 33 +++++ calls/api/overview.mdx | 277 +++++++++++++++++++++++++++++++++------ 3 files changed, 311 insertions(+), 42 deletions(-) diff --git a/calls/api/get-call.mdx b/calls/api/get-call.mdx index 89b4fbc3..b6a9f198 100644 --- a/calls/api/get-call.mdx +++ b/calls/api/get-call.mdx @@ -1,3 +1,46 @@ --- +title: "Get Call" +sidebarTitle: "Get Call" openapi: get /calls/{sessionId} --- + +Retrieve detailed information about a specific call using its session ID. This endpoint returns complete call data including all participants, their individual metrics, and recording information. + +## When to Use + +| Scenario | Description | +|----------|-------------| +| Call details page | Display comprehensive information about a completed call | +| Recording access | Get the recording URL for playback or download | +| Participant analytics | View individual participant metrics (join time, duration, etc.) | +| Debugging | Investigate issues with a specific call session | + +## Example Request + +```bash +curl -X GET "https://{appId}.call-{region}.cometchat.io/v3/calls/v1.us.31780434a95d45.16923681138d75114d60d1345a22e4cc612263fb26c0b5cf92" \ + -H "apikey: YOUR_REST_API_KEY" +``` + +## Response Details + +The response includes: + +- **Call metadata**: Type, mode, status, duration, timestamps +- **Participants array**: Each participant's UID, device ID, join/leave times, and individual audio/video minutes +- **Recordings array**: Recording IDs, URLs, duration, and timestamps (if `hasRecording` is true) + + +The `sessionId` is returned when a call is initiated via the SDK or can be found in the [List Calls](/calls/api/list-calls) response. + + +## Participant States + +Each participant in the response has a `state` field: + +| State | Description | +|-------|-------------| +| `ongoing` | Participant is currently in the call | +| `ended` | Participant left the call normally | +| `unanswered` | Participant didn't answer the call | +| `rejected` | Participant rejected the call | diff --git a/calls/api/list-calls.mdx b/calls/api/list-calls.mdx index 7e91dcd3..0ba3dc35 100644 --- a/calls/api/list-calls.mdx +++ b/calls/api/list-calls.mdx @@ -1,3 +1,36 @@ --- +title: "List Calls" +sidebarTitle: "List Calls" openapi: get /calls --- + +Retrieve a paginated list of all calls in your application. Use query parameters to filter by call type, status, date range, and more. + +## Common Use Cases + +| Use Case | Query Parameters | +|----------|------------------| +| Get all video calls | `type=video` | +| Get ongoing calls | `status=ongoing` | +| Get calls with recordings | `hasRecording=true` | +| Get calls for a specific user | `uid=user123` | +| Get group calls only | `receiverType=group` | +| Get calls from a specific date | `startedAt=1692368000` | + +## Example Request + +```bash +curl -X GET "https://{appId}.call-{region}.cometchat.io/v3/calls?type=video&status=ended&hasRecording=true" \ + -H "apikey: YOUR_REST_API_KEY" +``` + +## Filtering Tips + +- Combine multiple filters to narrow results (e.g., `type=video&status=ended`) +- Use `startedAt` and `endedAt` for date range queries (Unix timestamps) +- Filter by `participantsCount` to find calls with specific attendance +- Use `uid` to get all calls involving a specific user + + +Results are paginated. Check the `meta.pagination` object in the response for total count and page information. + diff --git a/calls/api/overview.mdx b/calls/api/overview.mdx index e32988ba..d309af3b 100644 --- a/calls/api/overview.mdx +++ b/calls/api/overview.mdx @@ -1,46 +1,239 @@ --- title: "Overview" +sidebarTitle: "Overview" --- -The Calls API provides programmatic access to the logs. Below, we have mentioned the key properties. - -| Parameters | Type | Description | -| -------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------------------------- | -| **sessionId** | string | Specifies the call's unique ID. | -| **receiverType** | string | Specifies the receiver type of the call. Possible values: users/groups. | -| **totalAudioMinutes** | float | Indicates the total audio minutes of the call. | -| **totalVideoMinutes** | float | Indicates the total video minutes of the call. | -| **totalDurationInMinutes** | float | Indicates the total call minutes. Basically addition of audio & video minutes. | -| **totalDuration** | string | Representation of total call minutes in timer format. | -| **hasRecording** | boolean | Indicates if the call has recording in it. | -| **mode** | string | It represents the mode of the call. Possible values: call/meet/presenter | -| **startedAt** | integer | 10-digit unix timestamp indicating when the call was initiated. | -| **status** | string | It represents the current status of the call. Possible values: initiated, ongoing, ended, unanswered, rejected, canceled. | -| **type** | string | Indicating the type of the call. Possible values: audio/video | -| **totalParticipants** | integer | Indicating the participants count of the call. If a user joins from multiple devices, it would be accounted separately. | -| **endedAt** | integer | 10-digit unix timestamp indicating when the call was ended. | -| **participants** | array of objects | It includes details of individual participants who were a part of the call. | - -## Participants - -| Parameters | Type | description | -| -------------------------- | ------- | -------------------------------------------------------------------------------- | -| **uid** | string | Indicates unique identifier of an user. | -| **totalAudioMinutes** | float | Indicates the total audio minutes that the user was a part of the call. | -| **totalVideoMinutes** | float | Indicates the total video minutes that the user was a part of the call. | -| **totalDurationInMinutes** | float | Indicates the total call minutes that the user was a part of. | -| **deviceId** | string | Unique identifier of the device from where the user joined the call. | -| **isJoined** | boolean | Indicates if the user joined the call. | -| **joinedAt** | integer | 10-digit Unix timestamp indicating the joining time of the user. | -| **state** | string | Current state of the user. Possible values: ongoing, ended, unanswered, rejected | -| **leftAt** | integer | 10-digit Unix timestamp indicating the leaving time of the user from the call. | - -## Recordings - -| Parameters | Type | description | -| ------------------ | ------- | ------------------------------------------------------------------ | -| **rid** | string | Indicates unique identifier of the recording. | -| **duration** | float | Indicates total recording time of the call. | -| **startTime** | integer | 10-digit Unix timestamp indicating when the recording was started. | -| **endTime** | integer | 10-digit Unix timestamp indicating when the recording was ended. | -| **recording\_url** | string | Indicates the S3 URL of the call recording. | +The Calls API provides programmatic access to call logs and analytics. Use these APIs to retrieve call history, participant details, duration metrics, and recording information for your application. + +## Base URL + +``` +https://{appId}.call-{region}.cometchat.io/v3 +``` + +| Variable | Description | +|----------|-------------| +| `appId` | Your CometChat App ID | +| `region` | Your app's region: `us`, `eu`, or `in` | + +## Authentication + +All API requests require authentication using your REST API Key in the header: + +```bash +curl -X GET "https://{appId}.call-{region}.cometchat.io/v3/calls" \ + -H "apikey: YOUR_REST_API_KEY" +``` + + +Use the REST API Key from your [CometChat Dashboard](https://app.cometchat.com). This key has full access scope and should only be used server-side. + + +--- + +## Available Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| [/calls](/calls/api/list-calls) | GET | List all calls with filtering options | +| [/calls/{sessionId}](/calls/api/get-call) | GET | Get details of a specific call | + +--- + +## Use Cases + +| Use Case | Endpoint | Description | +|----------|----------|-------------| +| Call history | List Calls | Display call logs in your app | +| Analytics dashboard | List Calls | Aggregate call duration and participant metrics | +| Call details page | Get Call | Show detailed information for a specific call | +| Recording access | Get Call | Retrieve recording URLs for playback | +| Billing reports | List Calls | Calculate audio/video minutes for billing | + +--- + +## Call Object + +The call object contains all information about a call session. + +| Property | Type | Description | +|----------|------|-------------| +| `sessionId` | string | Unique identifier for the call | +| `type` | string | Call type: `audio` or `video` | +| `mode` | string | Call mode: `call`, `meet`, or `presenter` | +| `status` | string | Current status: `initiated`, `ongoing`, `ended`, `unanswered`, `rejected`, `canceled` | +| `receiverType` | string | Receiver type: `user` or `group` | +| `initiator` | string | UID of the user who initiated the call | +| `receiver` | string | UID of the user or GUID of the group receiving the call | +| `totalParticipants` | integer | Number of participants (multiple devices counted separately) | +| `totalAudioMinutes` | float | Total audio minutes across all participants | +| `totalVideoMinutes` | float | Total video minutes across all participants | +| `totalDurationInMinutes` | float | Total call duration (audio + video minutes) | +| `totalDuration` | string | Duration in timer format (e.g., "00:05:30") | +| `hasRecording` | boolean | Whether the call has recordings | +| `initiatedAt` | integer | Unix timestamp when call was initiated | +| `startedAt` | integer | Unix timestamp when call started (first participant joined) | +| `endedAt` | integer | Unix timestamp when call ended | +| `participants` | array | List of participant objects | +| `recordings` | array | List of recording objects (if `hasRecording` is true) | + +### Call Status Values + +| Status | Description | +|--------|-------------| +| `initiated` | Call has been initiated but no one has joined yet | +| `ongoing` | Call is currently in progress | +| `ended` | Call has ended normally | +| `unanswered` | Call was not answered within the timeout period | +| `rejected` | Receiver rejected the call | +| `canceled` | Caller canceled before receiver answered | + +### Call Mode Values + +| Mode | Description | +|------|-------------| +| `call` | Standard 1-on-1 or group call initiated via SDK | +| `meet` | Meeting/conference call with a shared session ID | +| `presenter` | Webinar-style call with presenter and viewers | + +--- + +## Participant Object + +Each participant in a call has the following properties: + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | string | Unique identifier of the user | +| `deviceId` | string | Unique identifier of the device used to join | +| `isJoined` | boolean | Whether the user actually joined the call | +| `state` | string | Participant state: `ongoing`, `ended`, `unanswered`, `rejected` | +| `joinedAt` | integer | Unix timestamp when participant joined | +| `leftAt` | integer | Unix timestamp when participant left | +| `totalAudioMinutes` | float | Audio minutes for this participant | +| `totalVideoMinutes` | float | Video minutes for this participant | +| `totalDurationInMinutes` | float | Total duration for this participant | + + +If a user joins from multiple devices, each device is counted as a separate participant entry. + + +--- + +## Recording Object + +When a call has recordings, each recording contains: + +| Property | Type | Description | +|----------|------|-------------| +| `rid` | string | Unique identifier of the recording | +| `recording_url` | string | S3 URL to download/stream the recording | +| `duration` | float | Recording duration in minutes | +| `startTime` | integer | Unix timestamp when recording started | +| `endTime` | integer | Unix timestamp when recording ended | + +--- + +## Example Response + +```json +{ + "data": { + "sessionId": "v1.us.31780434a95d45.16923681138d75114d60d1345a22e4cc612263fb26c0b5cf92", + "type": "audio", + "mode": "call", + "status": "ended", + "receiverType": "user", + "initiator": "superhero8", + "receiver": "superhero2", + "totalParticipants": 2, + "totalAudioMinutes": 0.32, + "totalVideoMinutes": 0, + "totalDurationInMinutes": 0.32, + "totalDuration": "00:00:19", + "hasRecording": false, + "initiatedAt": 1692368113, + "startedAt": 1692368127, + "endedAt": 1692368146, + "participants": [ + { + "uid": "superhero8", + "deviceId": "70ecae89-b71c-4bb3-8220-b7c99ec1658f@rtc.cometchat.com/hsYWb5ul", + "isJoined": true, + "state": "ended", + "joinedAt": 1692368128, + "leftAt": 1692368144, + "totalAudioMinutes": 0.27, + "totalVideoMinutes": 0, + "totalDurationInMinutes": 0.27 + }, + { + "uid": "superhero2", + "deviceId": "c9ed493e-8495-428d-b6ee-b32019cc57ce@rtc.cometchat.com/CKT3xgR4", + "isJoined": true, + "state": "ended", + "joinedAt": 1692368132, + "leftAt": 1692368146, + "totalAudioMinutes": 0.23, + "totalVideoMinutes": 0, + "totalDurationInMinutes": 0.23 + } + ] + } +} +``` + +--- + +## Pagination + +List endpoints return paginated results with metadata: + +```json +{ + "data": [...], + "meta": { + "pagination": { + "total": 150, + "count": 25, + "per_page": 25, + "current_page": 1, + "total_pages": 6 + } + } +} +``` + +| Property | Description | +|----------|-------------| +| `total` | Total number of records | +| `count` | Number of records in current response | +| `per_page` | Records per page | +| `current_page` | Current page number | +| `total_pages` | Total number of pages | + +--- + +## Error Handling + +The API returns standard HTTP status codes: + +| Status Code | Description | +|-------------|-------------| +| `200` | Success | +| `400` | Bad request - Invalid parameters | +| `401` | Unauthorized - Invalid or missing API key | +| `404` | Not found - Call session doesn't exist | +| `429` | Rate limited - Too many requests | +| `500` | Server error | + +Error responses include a message: + +```json +{ + "error": { + "code": "ERR_SESSION_NOT_FOUND", + "message": "The specified session ID does not exist" + } +} +``` From 6a5a8ced20fe0d0821a2284b09a906081759b0d1 Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Wed, 14 Jan 2026 20:37:10 +0530 Subject: [PATCH 05/10] docs(calls/android): add in-call chat and share invite documentation - Add in-call-chat.mdx: CometChat UI Kit integration for messaging during calls - Add share-invite.mdx: Share button functionality with deep link handling - Update docs.json navigation to include new pages in Advanced group --- calls/android/in-call-chat.mdx | 830 +++++++++++++++++++++++++++++++++ calls/android/share-invite.mdx | 559 ++++++++++++++++++++++ docs.json | 4 +- 3 files changed, 1392 insertions(+), 1 deletion(-) create mode 100644 calls/android/in-call-chat.mdx create mode 100644 calls/android/share-invite.mdx diff --git a/calls/android/in-call-chat.mdx b/calls/android/in-call-chat.mdx new file mode 100644 index 00000000..8274e55f --- /dev/null +++ b/calls/android/in-call-chat.mdx @@ -0,0 +1,830 @@ +--- +title: "In-Call Chat" +sidebarTitle: "In-Call Chat" +--- + +Add real-time messaging to your call experience using CometChat UI Kit. This allows participants to send text messages, share files, and communicate via chat while on a call. + +## Overview + +In-call chat creates a group conversation linked to the call session. When participants tap the chat button, they can: +- Send and receive text messages +- Share images, files, and media +- See message history from the current call +- Get unread message notifications via badge count + +```mermaid +flowchart LR + subgraph "Call Session" + A[Call UI] --> B[Chat Button] + B --> C[Chat Activity] + end + + subgraph "CometChat" + D[Group] --> E[Messages] + end + + C <--> D + A -->|Session ID = Group GUID| D +``` + +## Prerequisites + +- CometChat Calls SDK integrated ([Setup](/calls/android/setup)) +- CometChat Chat SDK integrated ([Chat SDK](/sdk/android/overview)) +- CometChat UI Kit integrated ([UI Kit](/ui-kit/android/overview)) + + +The Chat SDK and UI Kit are separate from the Calls SDK. You'll need to add both dependencies to your project. + + +--- + +## Step 1: Add UI Kit Dependency + +Add the CometChat UI Kit to your `build.gradle`: + +```groovy +dependencies { + implementation 'com.cometchat:chat-uikit-android:4.+' +} +``` + +--- + +## Step 2: Enable Chat Button + +Configure session settings to show the chat button: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideChatButton(false) // Show the chat button + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideChatButton(false) // Show the chat button + .build(); +``` + + + +--- + +## Step 3: Create Chat Group + +Create or join a CometChat group using the session ID as the group GUID. This links the chat to the specific call session. + + + +```kotlin +private fun setupChatGroup(sessionId: String, meetingName: String) { + // Try to get existing group first + CometChat.getGroup(sessionId, object : CometChat.CallbackListener() { + override fun onSuccess(group: Group) { + if (!group.isJoined) { + // Join the existing group + joinGroup(sessionId, group.groupType) + } else { + Log.d(TAG, "Already joined group: ${group.name}") + } + } + + override fun onError(e: CometChatException) { + if (e.code == "ERR_GUID_NOT_FOUND") { + // Group doesn't exist, create it + createGroup(sessionId, meetingName) + } else { + Log.e(TAG, "Error getting group: ${e.message}") + } + } + }) +} + +private fun createGroup(guid: String, name: String) { + val group = Group(guid, name, CometChatConstants.GROUP_TYPE_PUBLIC, null) + + CometChat.createGroup(group, object : CometChat.CallbackListener() { + override fun onSuccess(createdGroup: Group) { + Log.d(TAG, "Group created: ${createdGroup.name}") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Group creation failed: ${e.message}") + } + }) +} + +private fun joinGroup(guid: String, groupType: String) { + CometChat.joinGroup(guid, groupType, null, object : CometChat.CallbackListener() { + override fun onSuccess(joinedGroup: Group) { + Log.d(TAG, "Joined group: ${joinedGroup.name}") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Join group failed: ${e.message}") + } + }) +} +``` + + +```java +private void setupChatGroup(String sessionId, String meetingName) { + // Try to get existing group first + CometChat.getGroup(sessionId, new CometChat.CallbackListener() { + @Override + public void onSuccess(Group group) { + if (!group.isJoined()) { + // Join the existing group + joinGroup(sessionId, group.getGroupType()); + } else { + Log.d(TAG, "Already joined group: " + group.getName()); + } + } + + @Override + public void onError(CometChatException e) { + if ("ERR_GUID_NOT_FOUND".equals(e.getCode())) { + // Group doesn't exist, create it + createGroup(sessionId, meetingName); + } else { + Log.e(TAG, "Error getting group: " + e.getMessage()); + } + } + }); +} + +private void createGroup(String guid, String name) { + Group group = new Group(guid, name, CometChatConstants.GROUP_TYPE_PUBLIC, null); + + CometChat.createGroup(group, new CometChat.CallbackListener() { + @Override + public void onSuccess(Group createdGroup) { + Log.d(TAG, "Group created: " + createdGroup.getName()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Group creation failed: " + e.getMessage()); + } + }); +} + +private void joinGroup(String guid, String groupType) { + CometChat.joinGroup(guid, groupType, null, new CometChat.CallbackListener() { + @Override + public void onSuccess(Group joinedGroup) { + Log.d(TAG, "Joined group: " + joinedGroup.getName()); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Join group failed: " + e.getMessage()); + } + }); +} +``` + + + +--- + +## Step 4: Handle Chat Button Click + +Listen for the chat button click and open your chat activity: + + + +```kotlin +private var unreadMessageCount = 0 + +private fun setupChatButtonListener() { + val callSession = CallSession.getInstance() + + callSession.addButtonClickListener(this, object : ButtonClickListener() { + override fun onChatButtonClicked() { + // Reset unread count when opening chat + unreadMessageCount = 0 + callSession.setChatButtonUnreadCount(0) + + // Open chat activity + val intent = Intent(this@CallActivity, ChatActivity::class.java).apply { + putExtra("SESSION_ID", sessionId) + putExtra("MEETING_NAME", meetingName) + } + startActivity(intent) + } + }) +} +``` + + +```java +private int unreadMessageCount = 0; + +private void setupChatButtonListener() { + CallSession callSession = CallSession.getInstance(); + + callSession.addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onChatButtonClicked() { + // Reset unread count when opening chat + unreadMessageCount = 0; + callSession.setChatButtonUnreadCount(0); + + // Open chat activity + Intent intent = new Intent(CallActivity.this, ChatActivity.class); + intent.putExtra("SESSION_ID", sessionId); + intent.putExtra("MEETING_NAME", meetingName); + startActivity(intent); + } + }); +} +``` + + + +--- + +## Step 5: Track Unread Messages + +Listen for incoming messages and update the badge count on the chat button: + + + +```kotlin +private fun setupMessageListener() { + CometChat.addMessageListener(TAG, object : CometChat.MessageListener() { + override fun onTextMessageReceived(textMessage: TextMessage) { + // Check if message is for our call's group + val receiver = textMessage.receiver + if (receiver is Group && receiver.guid == sessionId) { + unreadMessageCount++ + CallSession.getInstance().setChatButtonUnreadCount(unreadMessageCount) + } + } + + override fun onMediaMessageReceived(mediaMessage: MediaMessage) { + val receiver = mediaMessage.receiver + if (receiver is Group && receiver.guid == sessionId) { + unreadMessageCount++ + CallSession.getInstance().setChatButtonUnreadCount(unreadMessageCount) + } + } + }) +} + +override fun onDestroy() { + super.onDestroy() + CometChat.removeMessageListener(TAG) +} +``` + + +```java +private void setupMessageListener() { + CometChat.addMessageListener(TAG, new CometChat.MessageListener() { + @Override + public void onTextMessageReceived(TextMessage textMessage) { + // Check if message is for our call's group + BaseMessage receiver = textMessage.getReceiver(); + if (receiver instanceof Group && ((Group) receiver).getGuid().equals(sessionId)) { + unreadMessageCount++; + CallSession.getInstance().setChatButtonUnreadCount(unreadMessageCount); + } + } + + @Override + public void onMediaMessageReceived(MediaMessage mediaMessage) { + BaseMessage receiver = mediaMessage.getReceiver(); + if (receiver instanceof Group && ((Group) receiver).getGuid().equals(sessionId)) { + unreadMessageCount++; + CallSession.getInstance().setChatButtonUnreadCount(unreadMessageCount); + } + } + }); +} + +@Override +protected void onDestroy() { + super.onDestroy(); + CometChat.removeMessageListener(TAG); +} +``` + + + +--- + +## Step 6: Create Chat Activity + +Create a chat activity using UI Kit components: + + +```xml + + + + + + + + + + + + +``` + + + + +```kotlin +class ChatActivity : AppCompatActivity() { + + private lateinit var messageList: CometChatMessageList + private lateinit var messageComposer: CometChatMessageComposer + private lateinit var messageHeader: CometChatMessageHeader + private lateinit var progressBar: ProgressBar + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_chat) + + messageList = findViewById(R.id.message_list) + messageComposer = findViewById(R.id.message_composer) + messageHeader = findViewById(R.id.message_header) + progressBar = findViewById(R.id.progress_bar) + + val sessionId = intent.getStringExtra("SESSION_ID") ?: return + val meetingName = intent.getStringExtra("MEETING_NAME") ?: "Chat" + + loadGroup(sessionId, meetingName) + } + + private fun loadGroup(guid: String, meetingName: String) { + progressBar.visibility = View.VISIBLE + + CometChat.getGroup(guid, object : CometChat.CallbackListener() { + override fun onSuccess(group: Group) { + if (!group.isJoined) { + joinAndSetGroup(guid, group.groupType) + } else { + setGroup(group) + } + } + + override fun onError(e: CometChatException) { + if (e.code == "ERR_GUID_NOT_FOUND") { + createAndSetGroup(guid, meetingName) + } else { + progressBar.visibility = View.GONE + Log.e(TAG, "Error: ${e.message}") + } + } + }) + } + + private fun createAndSetGroup(guid: String, name: String) { + val group = Group(guid, name, CometChatConstants.GROUP_TYPE_PUBLIC, null) + CometChat.createGroup(group, object : CometChat.CallbackListener() { + override fun onSuccess(createdGroup: Group) { + setGroup(createdGroup) + } + + override fun onError(e: CometChatException) { + progressBar.visibility = View.GONE + } + }) + } + + private fun joinAndSetGroup(guid: String, groupType: String) { + CometChat.joinGroup(guid, groupType, null, object : CometChat.CallbackListener() { + override fun onSuccess(joinedGroup: Group) { + setGroup(joinedGroup) + } + + override fun onError(e: CometChatException) { + progressBar.visibility = View.GONE + } + }) + } + + private fun setGroup(group: Group) { + progressBar.visibility = View.GONE + + messageList.setGroup(group) + messageComposer.setGroup(group) + messageHeader.setGroup(group) + + // Hide auxiliary buttons (call, video) since we're already in a call + messageHeader.setAuxiliaryButtonView { _, _, _ -> + View(this).apply { visibility = View.GONE } + } + } + + companion object { + private const val TAG = "ChatActivity" + } +} +``` + + +```java +public class ChatActivity extends AppCompatActivity { + + private static final String TAG = "ChatActivity"; + + private CometChatMessageList messageList; + private CometChatMessageComposer messageComposer; + private CometChatMessageHeader messageHeader; + private ProgressBar progressBar; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_chat); + + messageList = findViewById(R.id.message_list); + messageComposer = findViewById(R.id.message_composer); + messageHeader = findViewById(R.id.message_header); + progressBar = findViewById(R.id.progress_bar); + + String sessionId = getIntent().getStringExtra("SESSION_ID"); + String meetingName = getIntent().getStringExtra("MEETING_NAME"); + + if (sessionId == null) return; + if (meetingName == null) meetingName = "Chat"; + + loadGroup(sessionId, meetingName); + } + + private void loadGroup(String guid, String meetingName) { + progressBar.setVisibility(View.VISIBLE); + + CometChat.getGroup(guid, new CometChat.CallbackListener() { + @Override + public void onSuccess(Group group) { + if (!group.isJoined()) { + joinAndSetGroup(guid, group.getGroupType()); + } else { + setGroup(group); + } + } + + @Override + public void onError(CometChatException e) { + if ("ERR_GUID_NOT_FOUND".equals(e.getCode())) { + createAndSetGroup(guid, meetingName); + } else { + progressBar.setVisibility(View.GONE); + Log.e(TAG, "Error: " + e.getMessage()); + } + } + }); + } + + private void createAndSetGroup(String guid, String name) { + Group group = new Group(guid, name, CometChatConstants.GROUP_TYPE_PUBLIC, null); + CometChat.createGroup(group, new CometChat.CallbackListener() { + @Override + public void onSuccess(Group createdGroup) { + setGroup(createdGroup); + } + + @Override + public void onError(CometChatException e) { + progressBar.setVisibility(View.GONE); + } + }); + } + + private void joinAndSetGroup(String guid, String groupType) { + CometChat.joinGroup(guid, groupType, null, new CometChat.CallbackListener() { + @Override + public void onSuccess(Group joinedGroup) { + setGroup(joinedGroup); + } + + @Override + public void onError(CometChatException e) { + progressBar.setVisibility(View.GONE); + } + }); + } + + private void setGroup(Group group) { + progressBar.setVisibility(View.GONE); + + messageList.setGroup(group); + messageComposer.setGroup(group); + messageHeader.setGroup(group); + + // Hide auxiliary buttons since we're already in a call + messageHeader.setAuxiliaryButtonView((context, user, group1) -> { + View view = new View(context); + view.setVisibility(View.GONE); + return view; + }); + } +} +``` + + + +--- + +## Complete Example + +Here's the complete CallActivity with in-call chat integration: + + + +```kotlin +class CallActivity : AppCompatActivity() { + + private lateinit var callSession: CallSession + private var sessionId: String = "" + private var meetingName: String = "" + private var unreadMessageCount = 0 + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + sessionId = intent.getStringExtra("SESSION_ID") ?: return + meetingName = intent.getStringExtra("MEETING_NAME") ?: "Meeting" + + callSession = CallSession.getInstance() + + // Setup chat group for this call + setupChatGroup(sessionId, meetingName) + + // Listen for chat button clicks + setupChatButtonListener() + + // Track incoming messages for badge + setupMessageListener() + + // Join the call + joinCall() + } + + private fun setupChatGroup(sessionId: String, meetingName: String) { + CometChat.getGroup(sessionId, object : CometChat.CallbackListener() { + override fun onSuccess(group: Group) { + if (!group.isJoined) { + CometChat.joinGroup(sessionId, group.groupType, null, + object : CometChat.CallbackListener() { + override fun onSuccess(g: Group) {} + override fun onError(e: CometChatException) {} + }) + } + } + + override fun onError(e: CometChatException) { + if (e.code == "ERR_GUID_NOT_FOUND") { + val group = Group(sessionId, meetingName, + CometChatConstants.GROUP_TYPE_PUBLIC, null) + CometChat.createGroup(group, object : CometChat.CallbackListener() { + override fun onSuccess(g: Group) {} + override fun onError(e: CometChatException) {} + }) + } + } + }) + } + + private fun setupChatButtonListener() { + callSession.addButtonClickListener(this, object : ButtonClickListener() { + override fun onChatButtonClicked() { + unreadMessageCount = 0 + callSession.setChatButtonUnreadCount(0) + + startActivity(Intent(this@CallActivity, ChatActivity::class.java).apply { + putExtra("SESSION_ID", sessionId) + putExtra("MEETING_NAME", meetingName) + }) + } + }) + } + + private fun setupMessageListener() { + CometChat.addMessageListener(TAG, object : CometChat.MessageListener() { + override fun onTextMessageReceived(textMessage: TextMessage) { + val receiver = textMessage.receiver + if (receiver is Group && receiver.guid == sessionId) { + unreadMessageCount++ + callSession.setChatButtonUnreadCount(unreadMessageCount) + } + } + }) + } + + private fun joinCall() { + val container = findViewById(R.id.callContainer) + + val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setTitle(meetingName) + .hideChatButton(false) + .build() + + CometChatCalls.joinSession( + sessionId = sessionId, + sessionSettings = sessionSettings, + view = container, + context = this, + listener = object : CometChatCalls.CallbackListener() { + override fun onSuccess(session: CallSession) { + Log.d(TAG, "Joined call") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Join failed: ${e.message}") + } + } + ) + } + + override fun onDestroy() { + super.onDestroy() + CometChat.removeMessageListener(TAG) + } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + + private static final String TAG = "CallActivity"; + + private CallSession callSession; + private String sessionId; + private String meetingName; + private int unreadMessageCount = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + sessionId = getIntent().getStringExtra("SESSION_ID"); + meetingName = getIntent().getStringExtra("MEETING_NAME"); + + if (sessionId == null) return; + if (meetingName == null) meetingName = "Meeting"; + + callSession = CallSession.getInstance(); + + // Setup chat group for this call + setupChatGroup(sessionId, meetingName); + + // Listen for chat button clicks + setupChatButtonListener(); + + // Track incoming messages for badge + setupMessageListener(); + + // Join the call + joinCall(); + } + + private void setupChatGroup(String sessionId, String meetingName) { + CometChat.getGroup(sessionId, new CometChat.CallbackListener() { + @Override + public void onSuccess(Group group) { + if (!group.isJoined()) { + CometChat.joinGroup(sessionId, group.getGroupType(), null, + new CometChat.CallbackListener() { + @Override public void onSuccess(Group g) {} + @Override public void onError(CometChatException e) {} + }); + } + } + + @Override + public void onError(CometChatException e) { + if ("ERR_GUID_NOT_FOUND".equals(e.getCode())) { + Group group = new Group(sessionId, meetingName, + CometChatConstants.GROUP_TYPE_PUBLIC, null); + CometChat.createGroup(group, new CometChat.CallbackListener() { + @Override public void onSuccess(Group g) {} + @Override public void onError(CometChatException e) {} + }); + } + } + }); + } + + private void setupChatButtonListener() { + callSession.addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onChatButtonClicked() { + unreadMessageCount = 0; + callSession.setChatButtonUnreadCount(0); + + Intent intent = new Intent(CallActivity.this, ChatActivity.class); + intent.putExtra("SESSION_ID", sessionId); + intent.putExtra("MEETING_NAME", meetingName); + startActivity(intent); + } + }); + } + + private void setupMessageListener() { + CometChat.addMessageListener(TAG, new CometChat.MessageListener() { + @Override + public void onTextMessageReceived(TextMessage textMessage) { + if (textMessage.getReceiver() instanceof Group) { + Group group = (Group) textMessage.getReceiver(); + if (group.getGuid().equals(sessionId)) { + unreadMessageCount++; + callSession.setChatButtonUnreadCount(unreadMessageCount); + } + } + } + }); + } + + private void joinCall() { + FrameLayout container = findViewById(R.id.callContainer); + + SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setTitle(meetingName) + .hideChatButton(false) + .build(); + + CometChatCalls.joinSession( + sessionId, + sessionSettings, + container, + this, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession session) { + Log.d(TAG, "Joined call"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Join failed: " + e.getMessage()); + } + } + ); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + CometChat.removeMessageListener(TAG); + } +} +``` + + + +--- + +## Related Documentation + +- [UI Kit Overview](/ui-kit/android/overview) - CometChat UI Kit components +- [Button Click Listener](/calls/android/button-click-listener) - Handle button clicks +- [SessionSettingsBuilder](/calls/android/session-settings) - Configure chat button visibility diff --git a/calls/android/share-invite.mdx b/calls/android/share-invite.mdx new file mode 100644 index 00000000..ec67277e --- /dev/null +++ b/calls/android/share-invite.mdx @@ -0,0 +1,559 @@ +--- +title: "Share Invite" +sidebarTitle: "Share Invite" +--- + +Enable participants to invite others to join a call by sharing a meeting link. The share invite button opens the system share sheet, allowing users to send the invite via any messaging app, email, or social media. + +## Overview + +The share invite feature: +- Generates a shareable meeting link with session ID +- Opens Android's native share sheet +- Works with any app that supports text sharing +- Can be triggered from the default button or custom UI + +```mermaid +flowchart LR + A[Share Button] --> B[Generate Link] + B --> C[Share Sheet] + C --> D[WhatsApp] + C --> E[Email] + C --> F[SMS] + C --> G[Other Apps] +``` + +## Prerequisites + +- CometChat Calls SDK integrated ([Setup](/calls/android/setup)) +- Active call session ([Join Session](/calls/android/join-session)) + +--- + +## Step 1: Enable Share Button + +Configure session settings to show the share invite button: + + + +```kotlin +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideShareInviteButton(false) // Show the share button + .build() +``` + + +```java +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideShareInviteButton(false) // Show the share button + .build(); +``` + + + +--- + +## Step 2: Handle Share Button Click + +Listen for the share button click using `ButtonClickListener`: + + + +```kotlin +private fun setupShareButtonListener() { + val callSession = CallSession.getInstance() + + callSession.addButtonClickListener(this, object : ButtonClickListener() { + override fun onShareInviteButtonClicked() { + shareInviteLink() + } + }) +} +``` + + +```java +private void setupShareButtonListener() { + CallSession callSession = CallSession.getInstance(); + + callSession.addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onShareInviteButtonClicked() { + shareInviteLink(); + } + }); +} +``` + + + +--- + +## Step 3: Generate and Share Link + +Create the meeting invite URL and open the share sheet: + + + +```kotlin +private fun shareInviteLink() { + val inviteUrl = generateInviteUrl(sessionId, meetingName) + + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, "Join my meeting: $meetingName") + putExtra(Intent.EXTRA_TEXT, inviteUrl) + } + + startActivity(Intent.createChooser(shareIntent, "Share meeting link")) +} + +private fun generateInviteUrl(sessionId: String, meetingName: String): String { + val encodedName = try { + java.net.URLEncoder.encode(meetingName, "UTF-8") + } catch (e: Exception) { + meetingName + } + + // Replace with your app's deep link or web URL + return "https://yourapp.com/join?sessionId=$sessionId&name=$encodedName" +} +``` + + +```java +private void shareInviteLink() { + String inviteUrl = generateInviteUrl(sessionId, meetingName); + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Join my meeting: " + meetingName); + shareIntent.putExtra(Intent.EXTRA_TEXT, inviteUrl); + + startActivity(Intent.createChooser(shareIntent, "Share meeting link")); +} + +private String generateInviteUrl(String sessionId, String meetingName) { + String encodedName; + try { + encodedName = java.net.URLEncoder.encode(meetingName, "UTF-8"); + } catch (Exception e) { + encodedName = meetingName; + } + + // Replace with your app's deep link or web URL + return "https://yourapp.com/join?sessionId=" + sessionId + "&name=" + encodedName; +} +``` + + + +--- + +## Custom Share Message + +Customize the share message with more details: + + + +```kotlin +private fun shareInviteLink() { + val inviteUrl = generateInviteUrl(sessionId, meetingName) + + val shareMessage = """ + 📞 Join my meeting: $meetingName + + Click the link below to join: + $inviteUrl + + Meeting ID: $sessionId + """.trimIndent() + + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_SUBJECT, "Meeting Invite: $meetingName") + putExtra(Intent.EXTRA_TEXT, shareMessage) + } + + startActivity(Intent.createChooser(shareIntent, "Share meeting link")) +} +``` + + +```java +private void shareInviteLink() { + String inviteUrl = generateInviteUrl(sessionId, meetingName); + + String shareMessage = "📞 Join my meeting: " + meetingName + "\n\n" + + "Click the link below to join:\n" + + inviteUrl + "\n\n" + + "Meeting ID: " + sessionId; + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_SUBJECT, "Meeting Invite: " + meetingName); + shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage); + + startActivity(Intent.createChooser(shareIntent, "Share meeting link")); +} +``` + + + +--- + +## Deep Link Handling + +To allow users to join directly from the shared link, implement deep link handling in your app. + +### Configure Deep Links + +Add intent filters to your `AndroidManifest.xml`: + +```xml + + + + + + + + + + + + + + + + + + +``` + +### Handle Deep Link + + + +```kotlin +class JoinActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + handleDeepLink(intent) + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + handleDeepLink(intent) + } + + private fun handleDeepLink(intent: Intent) { + val data = intent.data ?: return + + val sessionId = data.getQueryParameter("sessionId") + val meetingName = data.getQueryParameter("name") + + if (sessionId != null) { + // Check if user is logged in + if (CometChat.getLoggedInUser() != null) { + joinCall(sessionId, meetingName ?: "Meeting") + } else { + // Save params and redirect to login + saveJoinParams(sessionId, meetingName) + startActivity(Intent(this, LoginActivity::class.java)) + } + } + } + + private fun joinCall(sessionId: String, meetingName: String) { + val intent = Intent(this, CallActivity::class.java).apply { + putExtra("SESSION_ID", sessionId) + putExtra("MEETING_NAME", meetingName) + } + startActivity(intent) + finish() + } + + private fun saveJoinParams(sessionId: String, meetingName: String?) { + getSharedPreferences("join_params", MODE_PRIVATE).edit().apply { + putString("sessionId", sessionId) + putString("meetingName", meetingName) + apply() + } + } +} +``` + + +```java +public class JoinActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + handleDeepLink(getIntent()); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handleDeepLink(intent); + } + + private void handleDeepLink(Intent intent) { + Uri data = intent.getData(); + if (data == null) return; + + String sessionId = data.getQueryParameter("sessionId"); + String meetingName = data.getQueryParameter("name"); + + if (sessionId != null) { + // Check if user is logged in + if (CometChat.getLoggedInUser() != null) { + joinCall(sessionId, meetingName != null ? meetingName : "Meeting"); + } else { + // Save params and redirect to login + saveJoinParams(sessionId, meetingName); + startActivity(new Intent(this, LoginActivity.class)); + } + } + } + + private void joinCall(String sessionId, String meetingName) { + Intent intent = new Intent(this, CallActivity.class); + intent.putExtra("SESSION_ID", sessionId); + intent.putExtra("MEETING_NAME", meetingName); + startActivity(intent); + finish(); + } + + private void saveJoinParams(String sessionId, String meetingName) { + getSharedPreferences("join_params", MODE_PRIVATE) + .edit() + .putString("sessionId", sessionId) + .putString("meetingName", meetingName) + .apply(); + } +} +``` + + + +--- + +## Custom Share Button + +If you want to use a custom share button instead of the default one, hide the default button and implement your own: + + + +```kotlin +// Hide default share button +val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .hideShareInviteButton(true) + .build() + +// Add your custom button +customShareButton.setOnClickListener { + shareInviteLink() +} +``` + + +```java +// Hide default share button +SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .hideShareInviteButton(true) + .build(); + +// Add your custom button +customShareButton.setOnClickListener(v -> shareInviteLink()); +``` + + + +--- + +## Complete Example + + + +```kotlin +class CallActivity : AppCompatActivity() { + + private var sessionId: String = "" + private var meetingName: String = "" + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_call) + + sessionId = intent.getStringExtra("SESSION_ID") ?: return + meetingName = intent.getStringExtra("MEETING_NAME") ?: "Meeting" + + setupShareButtonListener() + joinCall() + } + + private fun setupShareButtonListener() { + CallSession.getInstance().addButtonClickListener(this, object : ButtonClickListener() { + override fun onShareInviteButtonClicked() { + shareInviteLink() + } + }) + } + + private fun shareInviteLink() { + val inviteUrl = generateInviteUrl(sessionId, meetingName) + + val shareMessage = """ + 📞 Join my meeting: $meetingName + + $inviteUrl + """.trimIndent() + + val shareIntent = Intent(Intent.ACTION_SEND).apply { + type = "text/plain" + putExtra(Intent.EXTRA_TEXT, shareMessage) + } + + startActivity(Intent.createChooser(shareIntent, "Share meeting link")) + } + + private fun generateInviteUrl(sessionId: String, meetingName: String): String { + val encodedName = java.net.URLEncoder.encode(meetingName, "UTF-8") + return "https://yourapp.com/join?sessionId=$sessionId&name=$encodedName" + } + + private fun joinCall() { + val container = findViewById(R.id.callContainer) + + val sessionSettings = CometChatCalls.SessionSettingsBuilder() + .setTitle(meetingName) + .hideShareInviteButton(false) + .build() + + CometChatCalls.joinSession( + sessionId = sessionId, + sessionSettings = sessionSettings, + view = container, + context = this, + listener = object : CometChatCalls.CallbackListener() { + override fun onSuccess(session: CallSession) { + Log.d(TAG, "Joined call") + } + + override fun onError(e: CometChatException) { + Log.e(TAG, "Join failed: ${e.message}") + } + } + ) + } + + companion object { + private const val TAG = "CallActivity" + } +} +``` + + +```java +public class CallActivity extends AppCompatActivity { + + private static final String TAG = "CallActivity"; + private String sessionId; + private String meetingName; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_call); + + sessionId = getIntent().getStringExtra("SESSION_ID"); + meetingName = getIntent().getStringExtra("MEETING_NAME"); + + if (sessionId == null) return; + if (meetingName == null) meetingName = "Meeting"; + + setupShareButtonListener(); + joinCall(); + } + + private void setupShareButtonListener() { + CallSession.getInstance().addButtonClickListener(this, new ButtonClickListener() { + @Override + public void onShareInviteButtonClicked() { + shareInviteLink(); + } + }); + } + + private void shareInviteLink() { + String inviteUrl = generateInviteUrl(sessionId, meetingName); + + String shareMessage = "📞 Join my meeting: " + meetingName + "\n\n" + inviteUrl; + + Intent shareIntent = new Intent(Intent.ACTION_SEND); + shareIntent.setType("text/plain"); + shareIntent.putExtra(Intent.EXTRA_TEXT, shareMessage); + + startActivity(Intent.createChooser(shareIntent, "Share meeting link")); + } + + private String generateInviteUrl(String sessionId, String meetingName) { + try { + String encodedName = java.net.URLEncoder.encode(meetingName, "UTF-8"); + return "https://yourapp.com/join?sessionId=" + sessionId + "&name=" + encodedName; + } catch (Exception e) { + return "https://yourapp.com/join?sessionId=" + sessionId; + } + } + + private void joinCall() { + FrameLayout container = findViewById(R.id.callContainer); + + SessionSettings sessionSettings = new CometChatCalls.SessionSettingsBuilder() + .setTitle(meetingName) + .hideShareInviteButton(false) + .build(); + + CometChatCalls.joinSession( + sessionId, + sessionSettings, + container, + this, + new CometChatCalls.CallbackListener() { + @Override + public void onSuccess(CallSession session) { + Log.d(TAG, "Joined call"); + } + + @Override + public void onError(CometChatException e) { + Log.e(TAG, "Join failed: " + e.getMessage()); + } + } + ); + } +} +``` + + + +--- + +## Related Documentation + +- [Button Click Listener](/calls/android/button-click-listener) - Handle button clicks +- [SessionSettingsBuilder](/calls/android/session-settings) - Configure share button visibility +- [Join Session](/calls/android/join-session) - Join a call session diff --git a/docs.json b/docs.json index 06b71ac0..ebda13ce 100644 --- a/docs.json +++ b/docs.json @@ -4873,7 +4873,9 @@ "calls/android/custom-control-panel", "calls/android/custom-participant-list", "calls/android/voip-calling", - "calls/android/background-handling" + "calls/android/background-handling", + "calls/android/in-call-chat", + "calls/android/share-invite" ] } ] From 2eae7466b2fb80ef10867d4e5bee19318cf55844 Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Thu, 15 Jan 2026 04:10:14 +0530 Subject: [PATCH 06/10] docs(calls): add comprehensive iOS, JavaScript, and React Native SDK documentation - Add complete iOS Calls SDK documentation covering setup, authentication, actions, audio modes, call layouts, call logs, custom UI components, events, idle timeout, in-call chat, join session, participant management, picture-in-picture, raise hand, recording, ringing, screen sharing, session settings, share invite, and VoIP calling - Add comprehensive JavaScript Calls SDK documentation including Angular, React, Next.js, Vue, and Ionic integration guides with setup, authentication, actions, call layouts, call logs, custom controls, device management, events, idle timeout, in-call chat, join session, participant management, permissions handling, picture-in-picture, raise hand, recording, ringing, screen sharing, session settings, share invite, and virtual background features - Add detailed React Native Calls SDK documentation with setup, authentication, actions, audio modes, background handling, call layouts, call logs, custom UI components, events, idle timeout, in-call chat, join session, participant management, picture-in-picture, raise hand, recording, ringing, screen sharing, session settings, share invite, and VoIP calling - Add MCP server configuration file for GitHub and Linear integration - Update main calls overview page and documentation index to reflect new SDK documentation structure - Remove Ionic card from main calls page as it is now covered under JavaScript integration guides - Expand Calls SDK documentation coverage to provide platform-specific implementation guides for all major platforms --- calls.mdx | 1 - calls/ios/actions.mdx | 432 ++++++ calls/ios/audio-modes.mdx | 197 +++ calls/ios/authentication.mdx | 188 +++ calls/ios/background-handling.mdx | 67 + calls/ios/call-layouts.mdx | 185 +++ calls/ios/call-logs.mdx | 249 +++ calls/ios/custom-control-panel.mdx | 454 ++++++ calls/ios/custom-participant-list.mdx | 647 ++++++++ calls/ios/events.mdx | 585 ++++++++ calls/ios/idle-timeout.mdx | 168 +++ calls/ios/in-call-chat.mdx | 530 +++++++ calls/ios/join-session.mdx | 278 ++++ calls/ios/overview.mdx | 103 +- calls/ios/participant-management.mdx | 229 +++ calls/ios/picture-in-picture.mdx | 174 +++ calls/ios/raise-hand.mdx | 178 +++ calls/ios/recording.mdx | 218 +++ calls/ios/ringing.mdx | 453 ++++++ calls/ios/screen-sharing.mdx | 122 ++ calls/ios/session-settings.mdx | 640 ++++++++ calls/ios/setup.mdx | 121 ++ calls/ios/share-invite.mdx | 591 ++++++++ calls/ios/voip-calling.mdx | 663 ++++++++ calls/javascript/actions.mdx | 362 +++++ calls/javascript/angular-integration.mdx | 602 ++++++++ calls/javascript/authentication.mdx | 153 ++ calls/javascript/call-layouts.mdx | 76 + calls/javascript/call-logs.mdx | 51 + calls/javascript/custom-control-panel.mdx | 210 +++ calls/javascript/device-management.mdx | 151 ++ calls/javascript/events.mdx | 530 +++++++ calls/javascript/idle-timeout.mdx | 59 + calls/javascript/in-call-chat.mdx | 74 + calls/javascript/ionic-integration.mdx | 1329 +++++++++++++++++ calls/javascript/join-session.mdx | 146 ++ calls/javascript/nextjs-integration.mdx | 557 +++++++ calls/javascript/overview.mdx | 151 +- calls/javascript/participant-management.mdx | 170 +++ calls/javascript/permissions-handling.mdx | 319 ++++ calls/javascript/picture-in-picture.mdx | 114 ++ calls/javascript/raise-hand.mdx | 69 + calls/javascript/react-integration.mdx | 453 ++++++ calls/javascript/recording.mdx | 108 ++ calls/javascript/ringing.mdx | 282 ++++ calls/javascript/screen-sharing.mdx | 96 ++ calls/javascript/session-settings.mdx | 293 ++++ calls/javascript/setup.mdx | 106 ++ calls/javascript/share-invite.mdx | 93 ++ calls/javascript/virtual-background.mdx | 81 + calls/javascript/vue-integration.mdx | 579 +++++++ calls/react-native/actions.mdx | 329 ++++ calls/react-native/audio-modes.mdx | 293 ++++ calls/react-native/authentication.mdx | 194 +++ calls/react-native/background-handling.mdx | 292 ++++ calls/react-native/call-layouts.mdx | 213 +++ calls/react-native/call-logs.mdx | 302 ++++ calls/react-native/custom-control-panel.mdx | 323 ++++ .../react-native/custom-participant-list.mdx | 513 +++++++ calls/react-native/events.mdx | 336 +++++ calls/react-native/idle-timeout.mdx | 152 ++ calls/react-native/in-call-chat.mdx | 485 ++++++ calls/react-native/join-session.mdx | 349 +++++ calls/react-native/overview.mdx | 109 +- calls/react-native/participant-management.mdx | 349 +++++ calls/react-native/picture-in-picture.mdx | 239 +++ calls/react-native/raise-hand.mdx | 213 +++ calls/react-native/recording.mdx | 242 +++ calls/react-native/ringing.mdx | 394 +++++ calls/react-native/screen-sharing.mdx | 252 ++++ calls/react-native/session-settings.mdx | 261 ++++ calls/react-native/setup.mdx | 143 ++ calls/react-native/share-invite.mdx | 360 +++++ calls/react-native/voip-calling.mdx | 291 ++++ docs.json | 199 ++- 75 files changed, 21488 insertions(+), 32 deletions(-) create mode 100644 calls/ios/actions.mdx create mode 100644 calls/ios/audio-modes.mdx create mode 100644 calls/ios/authentication.mdx create mode 100644 calls/ios/background-handling.mdx create mode 100644 calls/ios/call-layouts.mdx create mode 100644 calls/ios/call-logs.mdx create mode 100644 calls/ios/custom-control-panel.mdx create mode 100644 calls/ios/custom-participant-list.mdx create mode 100644 calls/ios/events.mdx create mode 100644 calls/ios/idle-timeout.mdx create mode 100644 calls/ios/in-call-chat.mdx create mode 100644 calls/ios/join-session.mdx create mode 100644 calls/ios/participant-management.mdx create mode 100644 calls/ios/picture-in-picture.mdx create mode 100644 calls/ios/raise-hand.mdx create mode 100644 calls/ios/recording.mdx create mode 100644 calls/ios/ringing.mdx create mode 100644 calls/ios/screen-sharing.mdx create mode 100644 calls/ios/session-settings.mdx create mode 100644 calls/ios/setup.mdx create mode 100644 calls/ios/share-invite.mdx create mode 100644 calls/ios/voip-calling.mdx create mode 100644 calls/javascript/actions.mdx create mode 100644 calls/javascript/angular-integration.mdx create mode 100644 calls/javascript/authentication.mdx create mode 100644 calls/javascript/call-layouts.mdx create mode 100644 calls/javascript/call-logs.mdx create mode 100644 calls/javascript/custom-control-panel.mdx create mode 100644 calls/javascript/device-management.mdx create mode 100644 calls/javascript/events.mdx create mode 100644 calls/javascript/idle-timeout.mdx create mode 100644 calls/javascript/in-call-chat.mdx create mode 100644 calls/javascript/ionic-integration.mdx create mode 100644 calls/javascript/join-session.mdx create mode 100644 calls/javascript/nextjs-integration.mdx create mode 100644 calls/javascript/participant-management.mdx create mode 100644 calls/javascript/permissions-handling.mdx create mode 100644 calls/javascript/picture-in-picture.mdx create mode 100644 calls/javascript/raise-hand.mdx create mode 100644 calls/javascript/react-integration.mdx create mode 100644 calls/javascript/recording.mdx create mode 100644 calls/javascript/ringing.mdx create mode 100644 calls/javascript/screen-sharing.mdx create mode 100644 calls/javascript/session-settings.mdx create mode 100644 calls/javascript/setup.mdx create mode 100644 calls/javascript/share-invite.mdx create mode 100644 calls/javascript/virtual-background.mdx create mode 100644 calls/javascript/vue-integration.mdx create mode 100644 calls/react-native/actions.mdx create mode 100644 calls/react-native/audio-modes.mdx create mode 100644 calls/react-native/authentication.mdx create mode 100644 calls/react-native/background-handling.mdx create mode 100644 calls/react-native/call-layouts.mdx create mode 100644 calls/react-native/call-logs.mdx create mode 100644 calls/react-native/custom-control-panel.mdx create mode 100644 calls/react-native/custom-participant-list.mdx create mode 100644 calls/react-native/events.mdx create mode 100644 calls/react-native/idle-timeout.mdx create mode 100644 calls/react-native/in-call-chat.mdx create mode 100644 calls/react-native/join-session.mdx create mode 100644 calls/react-native/participant-management.mdx create mode 100644 calls/react-native/picture-in-picture.mdx create mode 100644 calls/react-native/raise-hand.mdx create mode 100644 calls/react-native/recording.mdx create mode 100644 calls/react-native/ringing.mdx create mode 100644 calls/react-native/screen-sharing.mdx create mode 100644 calls/react-native/session-settings.mdx create mode 100644 calls/react-native/setup.mdx create mode 100644 calls/react-native/share-invite.mdx create mode 100644 calls/react-native/voip-calling.mdx diff --git a/calls.mdx b/calls.mdx index 15bf6696..eea8d3b5 100644 --- a/calls.mdx +++ b/calls.mdx @@ -84,7 +84,6 @@ import { CardGroup, Card, Icon, Badge, Steps, Columns, AccordionGroup, Accordion } href="/calls/ios/overview" horizontal /> } href="/calls/android/overview" horizontal /> } href="/calls/flutter/overview" horizontal /> - } href="/calls/ionic/overview" horizontal /> diff --git a/calls/ios/actions.mdx b/calls/ios/actions.mdx new file mode 100644 index 00000000..2b20e2f0 --- /dev/null +++ b/calls/ios/actions.mdx @@ -0,0 +1,432 @@ +--- +title: "Actions" +sidebarTitle: "Actions" +--- + +Use call actions to create your own custom controls or trigger call functionality dynamically based on your use case. All actions are called on the `CallSession.shared` singleton instance during an active call session. + +## Get CallSession Instance + +The `CallSession` is a singleton that manages the active call. All actions are accessed through this instance. + + + +```swift +let callSession = CallSession.shared +``` + + +```objectivec +CallSession *callSession = [CallSession shared]; +``` + + + + +Always check `isCallSessionActive()` before calling actions to ensure there's an active call. + + +## Actions + +### Mute Audio + +Mutes your local microphone, stopping audio transmission to other participants. + + + +```swift +CallSession.shared.muteAudio() +``` + + +```objectivec +[[CallSession shared] muteAudio]; +``` + + + +### Unmute Audio + +Unmutes your local microphone, resuming audio transmission. + + + +```swift +CallSession.shared.unMuteAudio() +``` + + +```objectivec +[[CallSession shared] unMuteAudio]; +``` + + + +### Set Audio Mode + +Changes the audio output device during a call. + + + +```swift +CallSession.shared.setAudioMode("SPEAKER") +CallSession.shared.setAudioMode("EARPIECE") +CallSession.shared.setAudioMode("BLUETOOTH") +CallSession.shared.setAudioMode("HEADPHONES") +``` + + +```objectivec +[[CallSession shared] setAudioMode:@"SPEAKER"]; +[[CallSession shared] setAudioMode:@"EARPIECE"]; +[[CallSession shared] setAudioMode:@"BLUETOOTH"]; +[[CallSession shared] setAudioMode:@"HEADPHONES"]; +``` + + + + +| Value | Description | +|-------|-------------| +| `SPEAKER` | Routes audio through device loudspeaker | +| `EARPIECE` | Routes audio through phone earpiece | +| `BLUETOOTH` | Routes audio through connected Bluetooth device | +| `HEADPHONES` | Routes audio through wired headphones | + + +### Pause Video + +Turns off your local camera, stopping video transmission. Other participants see your avatar. + + + +```swift +CallSession.shared.pauseVideo() +``` + + +```objectivec +[[CallSession shared] pauseVideo]; +``` + + + +### Resume Video + +Turns on your local camera, resuming video transmission. + + + +```swift +CallSession.shared.resumeVideo() +``` + + +```objectivec +[[CallSession shared] resumeVideo]; +``` + + + +### Switch Camera + +Toggles between front and back cameras without interrupting the video stream. + + + +```swift +CallSession.shared.switchCamera() +``` + + +```objectivec +[[CallSession shared] switchCamera]; +``` + + + +### Start Recording + +Begins server-side recording of the call. All participants are notified. + + + +```swift +CallSession.shared.startRecording() +``` + + +```objectivec +[[CallSession shared] startRecording]; +``` + + + + +Recording requires the feature to be enabled for your CometChat app. + + +### Stop Recording + +Stops the current recording. The recording is saved and accessible via the dashboard. + + + +```swift +CallSession.shared.stopRecording() +``` + + +```objectivec +[[CallSession shared] stopRecording]; +``` + + + +### Mute Participant + +Mutes a specific participant's audio. This is a moderator action. + + + +```swift +CallSession.shared.muteParticipant(participantId: participant.pid) +``` + + +```objectivec +[[CallSession shared] muteParticipantWithParticipantId:participant.pid]; +``` + + + +### Pause Participant Video + +Pauses a specific participant's video. This is a moderator action. + + + +```swift +CallSession.shared.pauseParticipantVideo(participantId: participant.pid) +``` + + +```objectivec +[[CallSession shared] pauseParticipantVideoWithParticipantId:participant.pid]; +``` + + + +### Pin Participant + +Pins a participant to keep them prominently displayed regardless of who is speaking. + + + +```swift +CallSession.shared.pinParticipant() +``` + + +```objectivec +[[CallSession shared] pinParticipant]; +``` + + + +### Unpin Participant + +Removes the pin, returning to automatic speaker highlighting. + + + +```swift +CallSession.shared.unPinParticipant() +``` + + +```objectivec +[[CallSession shared] unPinParticipant]; +``` + + + +### Set Layout + +Changes the call layout. Each participant can choose their own layout independently. + + + +```swift +CallSession.shared.setLayout("TILE") +CallSession.shared.setLayout("SPOTLIGHT") +CallSession.shared.setLayout("SIDEBAR") +``` + + +```objectivec +[[CallSession shared] setLayout:@"TILE"]; +[[CallSession shared] setLayout:@"SPOTLIGHT"]; +[[CallSession shared] setLayout:@"SIDEBAR"]; +``` + + + + +| Value | Description | +|-------|-------------| +| `TILE` | Grid layout with equally-sized tiles | +| `SPOTLIGHT` | Large view for active speaker, small tiles for others | +| `SIDEBAR` | Main speaker with participants in a sidebar | + + +### Enable Picture In Picture Layout + +Enables PiP mode, allowing the call to continue in a floating window. + + + +```swift +CallSession.shared.enablePictureInPictureLayout() +``` + + +```objectivec +[[CallSession shared] enablePictureInPictureLayout]; +``` + + + +### Disable Picture In Picture Layout + +Disables PiP mode, returning to full-screen call interface. + + + +```swift +CallSession.shared.disablePictureInPictureLayout() +``` + + +```objectivec +[[CallSession shared] disablePictureInPictureLayout]; +``` + + + +### Set Chat Button Unread Count + +Updates the badge count on the chat button. Pass 0 to hide the badge. + + + +```swift +CallSession.shared.setChatButtonUnreadCount(5) +``` + + +```objectivec +[[CallSession shared] setChatButtonUnreadCount:5]; +``` + + + +### Is Session Active + +Returns `true` if a call session is active, `false` otherwise. + + + +```swift +let isActive = CallSession.shared.isCallSessionActive() +``` + + +```objectivec +BOOL isActive = [[CallSession shared] isCallSessionActive]; +``` + + + +### Leave Session + +Ends your participation and disconnects gracefully. The call continues for other participants. + + + +```swift +CallSession.shared.leaveSession() +``` + + +```objectivec +[[CallSession shared] leaveSession]; +``` + + + +### Raise Hand + +Shows a hand-raised indicator to get attention from other participants. + + + +```swift +CallSession.shared.raiseHand() +``` + + +```objectivec +[[CallSession shared] raiseHand]; +``` + + + +### Lower Hand + +Removes the hand-raised indicator. + + + +```swift +CallSession.shared.lowerHand() +``` + + +```objectivec +[[CallSession shared] lowerHand]; +``` + + + +### Hide Settings Panel + +Hides the settings panel if it's currently visible. + + + +```swift +CallSession.shared.hideSettingsPanel() +``` + + +```objectivec +[[CallSession shared] hideSettingsPanel]; +``` + + + + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | `String` | Unique identifier (CometChat user ID) | +| `name` | `String` | Display name | +| `avatar` | `String` | URL of avatar image | +| `pid` | `String` | Participant ID for this call session | +| `role` | `String` | Role in the call | +| `audioMuted` | `Bool` | Whether audio is muted | +| `videoPaused` | `Bool` | Whether video is paused | +| `isPinned` | `Bool` | Whether pinned in layout | +| `isPresenting` | `Bool` | Whether screen sharing | +| `raisedHandTimestamp` | `Int` | Timestamp when hand was raised | + diff --git a/calls/ios/audio-modes.mdx b/calls/ios/audio-modes.mdx new file mode 100644 index 00000000..ab53af7f --- /dev/null +++ b/calls/ios/audio-modes.mdx @@ -0,0 +1,197 @@ +--- +title: "Audio Modes" +sidebarTitle: "Audio Modes" +--- + +Control audio output routing during calls. Switch between speaker, earpiece, Bluetooth, and wired headphones based on user preference or device availability. + +## Available Audio Modes + +| Mode | Description | +|------|-------------| +| `.speaker` | Routes audio through the device loudspeaker | +| `.earpiece` | Routes audio through the phone earpiece (for private calls) | +| `.bluetooth` | Routes audio through a connected Bluetooth device | +| `.headphones` | Routes audio through wired headphones | + +## Set Initial Audio Mode + +Configure the audio mode when joining a session: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setAudioMode(.speaker) + .build() + +CometChatCalls.joinSession( + sessionID: sessionId, + callSetting: sessionSettings, + container: callViewContainer, + onSuccess: { message in + print("Joined with speaker mode") + }, + onError: { error in + print("Failed: \(error?.errorDescription ?? "")") + } +) +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + setAudioMode:AudioModeTypeSpeaker] + build]; + +[CometChatCalls joinSessionWithSessionID:sessionId + callSetting:sessionSettings + container:self.callViewContainer + onSuccess:^(NSString * message) { + NSLog(@"Joined with speaker mode"); +} onError:^(CometChatCallException * error) { + NSLog(@"Failed: %@", error.errorDescription); +}]; +``` + + + +## Change Audio Mode During Call + +Switch audio modes dynamically during an active call: + + + +```swift +// Switch to speaker +CallSession.shared.setAudioMode("SPEAKER") + +// Switch to earpiece +CallSession.shared.setAudioMode("EARPIECE") + +// Switch to Bluetooth +CallSession.shared.setAudioMode("BLUETOOTH") + +// Switch to wired headphones +CallSession.shared.setAudioMode("HEADPHONES") +``` + + +```objectivec +// Switch to speaker +[[CallSession shared] setAudioMode:@"SPEAKER"]; + +// Switch to earpiece +[[CallSession shared] setAudioMode:@"EARPIECE"]; + +// Switch to Bluetooth +[[CallSession shared] setAudioMode:@"BLUETOOTH"]; + +// Switch to wired headphones +[[CallSession shared] setAudioMode:@"HEADPHONES"]; +``` + + + +## Listen for Audio Mode Changes + +Monitor audio mode changes using `MediaEventsListener`: + + + +```swift +class CallViewController: UIViewController, MediaEventsListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addMediaEventsListener(self) + } + + deinit { + CallSession.shared.removeMediaEventsListener(self) + } + + func onAudioModeChanged(audioModeType: AudioModeType) { + switch audioModeType { + case .speaker: + print("Switched to speaker") + case .earpiece: + print("Switched to earpiece") + case .bluetooth: + print("Switched to Bluetooth") + case .headphones: + print("Switched to headphones") + default: + break + } + // Update audio mode button icon + updateAudioModeIcon(audioModeType) + } + + // Other callbacks... + func onAudioMuted() {} + func onAudioUnMuted() {} + func onVideoPaused() {} + func onVideoResumed() {} + func onRecordingStarted() {} + func onRecordingStopped() {} + func onScreenShareStarted() {} + func onScreenShareStopped() {} + func onCameraFacingChanged(cameraFacing: CameraFacing) {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addMediaEventsListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeMediaEventsListener:self]; +} + +- (void)onAudioModeChangedWithAudioModeType:(AudioModeType)audioModeType { + // Update audio mode button icon + [self updateAudioModeIcon:audioModeType]; +} + +// Other callbacks... + +@end +``` + + + +## Hide Audio Mode Button + +To prevent users from changing the audio mode, hide the button in the call UI: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setAudioMode(.speaker) // Fixed audio mode + .hideAudioModeButton(true) // Hide toggle button + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] + setAudioMode:AudioModeTypeSpeaker] + hideAudioModeButton:YES] + build]; +``` + + + + +The SDK automatically detects connected audio devices. If Bluetooth or wired headphones are connected, they become available as audio mode options. + diff --git a/calls/ios/authentication.mdx b/calls/ios/authentication.mdx new file mode 100644 index 00000000..058c8032 --- /dev/null +++ b/calls/ios/authentication.mdx @@ -0,0 +1,188 @@ +--- +title: "Authentication" +sidebarTitle: "Authentication" +--- + +Before users can make or receive calls, they must be authenticated with the CometChat Calls SDK. This guide covers the login and logout methods. + + +**Sample Users** + +CometChat provides 5 test users: `cometchat-uid-1`, `cometchat-uid-2`, `cometchat-uid-3`, `cometchat-uid-4`, and `cometchat-uid-5`. + + +## Check Login Status + +Before calling `login()`, check if a user is already logged in using `getLoggedInUser()`. The SDK maintains the session internally, so you only need to login once per user session. + + + +```swift +if let loggedInUser = CometChatCalls.getLoggedInUser() { + // User is already logged in + print("User already logged in: \(loggedInUser.uid ?? "")") +} else { + // No user logged in, proceed with login +} +``` + + +```objectivec +CallsUser *loggedInUser = [CometChatCalls getLoggedInUser]; + +if (loggedInUser != nil) { + // User is already logged in + NSLog(@"User already logged in: %@", loggedInUser.uid); +} else { + // No user logged in, proceed with login +} +``` + + + +The `getLoggedInUser()` method returns a `CallsUser` object if a user is logged in, or `nil` if no session exists. + +## Login with UID and API Key + +This method is suitable for development and testing. For production apps, use [Auth Token login](#login-with-auth-token) instead. + + +**Security Notice** + +Using the API Key directly in client code is not recommended for production. Use Auth Token authentication for enhanced security. + + + + +```swift +let uid = "cometchat-uid-1" // Replace with your user's UID +let apiKey = "API_KEY" // Replace with your API Key + +if CometChatCalls.getLoggedInUser() == nil { + CometChatCalls.login(UID: uid, apiKey: apiKey, onSuccess: { user in + print("Login successful: \(user.uid ?? "")") + }, onError: { error in + print("Login failed: \(error.errorDescription)") + }) +} else { + // User already logged in +} +``` + + +```objectivec +NSString *uid = @"cometchat-uid-1"; // Replace with your user's UID +NSString *apiKey = @"API_KEY"; // Replace with your API Key + +if ([CometChatCalls getLoggedInUser] == nil) { + [CometChatCalls loginWithUID:uid + apiKey:apiKey + onSuccess:^(CallsUser * user) { + NSLog(@"Login successful: %@", user.uid); + } onError:^(CometChatCallException error) { + NSLog(@"Login failed: %@", error.errorDescription); + }]; +} else { + // User already logged in +} +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `UID` | String | The unique identifier of the user to login | +| `apiKey` | String | Your CometChat API Key | +| `onSuccess` | Closure | Closure called with `CallsUser` on success | +| `onError` | Closure | Closure called with error on failure | + +## Login with Auth Token + +This is the recommended authentication method for production applications. The Auth Token is generated server-side, keeping your API Key secure. + +### Auth Token Flow + +1. User authenticates with your backend +2. Your backend calls the [CometChat Create Auth Token API](https://api-explorer.cometchat.com/reference/create-authtoken) +3. Your backend returns the Auth Token to the client +4. Client uses the Auth Token to login + + + +```swift +let authToken = "AUTH_TOKEN" // Token received from your backend + +CometChatCalls.login(authToken: authToken, onSuccess: { user in + print("Login successful: \(user.uid ?? "")") +}, onError: { error in + print("Login failed: \(error.errorDescription)") +}) +``` + + +```objectivec +NSString *authToken = @"AUTH_TOKEN"; // Token received from your backend + +[CometChatCalls loginWithAuthToken:authToken + onSuccess:^(CallsUser * user) { + NSLog(@"Login successful: %@", user.uid); +} onError:^(CometChatCallException error) { + NSLog(@"Login failed: %@", error.errorDescription); +}]; +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `authToken` | String | Auth Token generated via CometChat API | +| `onSuccess` | Closure | Closure called with `CallsUser` on success | +| `onError` | Closure | Closure called with error on failure | + +## CallsUser Object + +On successful login, the callback returns a `CallsUser` object containing user information: + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | String? | Unique identifier of the user | +| `name` | String? | Display name of the user | +| `avatar` | String? | URL of the user's avatar image | +| `status` | UserStatus | User's online status | + +## Logout + +Call `logout()` when the user signs out of your application. This clears the local session and disconnects from CometChat services. + + + +```swift +CometChatCalls.logout(onSuccess: { message in + print("Logout successful") +}, onError: { error in + print("Logout failed: \(error.errorDescription)") +}) +``` + + +```objectivec +[CometChatCalls logoutOnSuccess:^(NSString * message) { + NSLog(@"Logout successful"); +} onError:^(CometChatCallException error) { + NSLog(@"Logout failed: %@", error.errorDescription); +}]; +``` + + + +## Error Handling + +Common authentication errors: + +| Error Code | Description | +|------------|-------------| +| `ERROR_INVALID_UID` | The provided UID is empty or invalid | +| `ERROR_UID_WITH_SPACE` | The UID contains spaces (not allowed) | +| `ERROR_API_KEY_NOT_FOUND` | The API Key is missing or invalid | +| `ERROR_BLANK_AUTHTOKEN` | The Auth Token is empty | +| `ERROR_AUTHTOKEN_WITH_SPACE` | The Auth Token contains spaces | diff --git a/calls/ios/background-handling.mdx b/calls/ios/background-handling.mdx new file mode 100644 index 00000000..039d209e --- /dev/null +++ b/calls/ios/background-handling.mdx @@ -0,0 +1,67 @@ +--- +title: "Background Handling" +sidebarTitle: "Background Handling" +--- + +Keep calls alive when users navigate away from your app. Background handling ensures the call continues running when users press the home button, switch to another app, or lock their device. + +## Overview + +When a user leaves your call view controller, iOS may suspend it to free resources. Proper background handling: +- Keeps the call session active in the background +- Maintains audio/video streams +- Allows users to return to the call seamlessly + +```mermaid +flowchart LR + subgraph "User in Call" + A[Call VC] --> B{User Action} + end + + B -->|Stays in app| A + B -->|Presses HOME| C[Background Mode] + B -->|Opens other app| C + B -->|Locks device| C + + C --> D[Call Continues] + D -->|Returns to app| A +``` + +## When to Use + +| Scenario | Solution | +|----------|----------| +| User stays in call view | No action needed | +| User presses HOME during call | **Use Background Modes** | +| User switches to another app | **Use Background Modes** | +| User locks device during call | **Use Background Modes** | +| Receiving calls when app is killed | [VoIP Calling](/calls/ios/voip-calling) handles this | + + +Background Handling is different from [VoIP Calling](/calls/ios/voip-calling). VoIP handles **receiving** calls when the app is not running. Background Handling keeps an **active** call alive when the user leaves the app. + + +--- + +## Enable Background Modes + +In Xcode, add the following background modes to your app: + +**1.** Go to your target's "Signing & Capabilities" tab + +**2.** Add "Background Modes" capability + +**3.** Enable the following modes: + - Audio, AirPlay, and Picture in Picture + - Voice over IP + + +The Calls SDK automatically handles audio session configuration. You only need to enable the background modes capability. + + +--- + +## Related Documentation + +- [VoIP Calling](/calls/ios/voip-calling) - Receive calls when app is killed +- [Events](/calls/ios/events) - Session status events diff --git a/calls/ios/call-layouts.mdx b/calls/ios/call-layouts.mdx new file mode 100644 index 00000000..d1240938 --- /dev/null +++ b/calls/ios/call-layouts.mdx @@ -0,0 +1,185 @@ +--- +title: "Call Layouts" +sidebarTitle: "Call Layouts" +--- + +Choose how participants are displayed during a call. The SDK provides multiple layout options to suit different use cases like team meetings, presentations, or one-on-one calls. + +## Available Layouts + +| Layout | Description | Best For | +|--------|-------------|----------| +| `.tile` | Grid layout with equally-sized tiles for all participants | Team meetings, group discussions | +| `.spotlight` | Large view for the other participant, small tile for yourself | One-on-one calls, presentations, webinars | +| `.sidebar` | Main speaker with participants in a sidebar | Interviews, panel discussions | + +## Set Initial Layout + +Configure the layout when joining a session using `SessionSettingsBuilder`: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setLayout(.tile) + .build() + +CometChatCalls.joinSession( + sessionID: sessionId, + callSetting: sessionSettings, + container: callViewContainer, + onSuccess: { message in + print("Joined with TILE layout") + }, + onError: { error in + print("Failed: \(error?.errorDescription ?? "")") + } +) +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + setLayout:LayoutTypeTile] + build]; + +[CometChatCalls joinSessionWithSessionID:sessionId + callSetting:sessionSettings + container:self.callViewContainer + onSuccess:^(NSString * message) { + NSLog(@"Joined with TILE layout"); +} onError:^(CometChatCallException * error) { + NSLog(@"Failed: %@", error.errorDescription); +}]; +``` + + + +## Change Layout During Call + +Switch layouts dynamically during an active call using `setLayout()`: + + + +```swift +// Switch to Spotlight layout +CallSession.shared.setLayout("SPOTLIGHT") + +// Switch to Tile layout +CallSession.shared.setLayout("TILE") + +// Switch to Sidebar layout +CallSession.shared.setLayout("SIDEBAR") +``` + + +```objectivec +// Switch to Spotlight layout +[[CallSession shared] setLayout:@"SPOTLIGHT"]; + +// Switch to Tile layout +[[CallSession shared] setLayout:@"TILE"]; + +// Switch to Sidebar layout +[[CallSession shared] setLayout:@"SIDEBAR"]; +``` + + + + +Each participant can choose their own layout independently. Changing your layout does not affect other participants. + + +## Listen for Layout Changes + +Monitor layout changes using `LayoutListener`: + + + +```swift +class CallViewController: UIViewController, LayoutListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addLayoutListener(self) + } + + deinit { + CallSession.shared.removeLayoutListener(self) + } + + func onCallLayoutChanged(layoutType: LayoutType) { + switch layoutType { + case .tile: + print("Switched to Tile layout") + case .spotlight: + print("Switched to Spotlight layout") + case .sidebar: + print("Switched to Sidebar layout") + default: + break + } + // Update layout toggle button icon + updateLayoutIcon(layoutType) + } + + func onParticipantListVisible() {} + func onParticipantListHidden() {} + func onPictureInPictureLayoutEnabled() {} + func onPictureInPictureLayoutDisabled() {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addLayoutListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeLayoutListener:self]; +} + +- (void)onCallLayoutChangedWithLayoutType:(LayoutType)layoutType { + // Update layout toggle button icon + [self updateLayoutIcon:layoutType]; +} + +- (void)onParticipantListVisible {} +- (void)onParticipantListHidden {} +- (void)onPictureInPictureLayoutEnabled {} +- (void)onPictureInPictureLayoutDisabled {} + +@end +``` + + + +## Hide Layout Toggle Button + +To prevent users from changing the layout, hide the layout toggle button in the call UI: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setLayout(.spotlight) // Fixed layout + .hideChangeLayoutButton(true) // Hide toggle button + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] + setLayout:LayoutTypeSpotlight] + hideChangeLayoutButton:YES] + build]; +``` + + diff --git a/calls/ios/call-logs.mdx b/calls/ios/call-logs.mdx new file mode 100644 index 00000000..ca116502 --- /dev/null +++ b/calls/ios/call-logs.mdx @@ -0,0 +1,249 @@ +--- +title: "Call Logs" +sidebarTitle: "Call Logs" +--- + +Retrieve call history for your application. Call logs provide detailed information about past calls including duration, participants, recordings, and status. + +## Fetch Call Logs + +Use `CallLogsRequest` to fetch call logs with pagination support. The builder pattern allows you to filter results by various criteria. + + + +```swift +let callLogRequest = CallLogsRequest.CallLogsRequestBuilder() + .setLimit(30) + .build() + +callLogRequest.fetchNext(onSuccess: { callLogs in + for callLog in callLogs { + print("Session: \(callLog.sessionID ?? "")") + print("Duration: \(callLog.totalDuration ?? "")") + print("Status: \(callLog.status ?? "")") + } +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + + +```objectivec +CallLogsRequest *callLogRequest = [[[CallLogsRequest CallLogsRequestBuilder] + setLimit:30] + build]; + +[callLogRequest fetchNextOnSuccess:^(NSArray * callLogs) { + for (CallLog *callLog in callLogs) { + NSLog(@"Session: %@", callLog.sessionID); + NSLog(@"Duration: %@", callLog.totalDuration); + NSLog(@"Status: %@", callLog.status); + } +} onError:^(CometChatCallException * error) { + NSLog(@"Error: %@", error.errorDescription); +}]; +``` + + + +## CallLogsRequestBuilder + +Configure the request using the builder methods: + +| Method | Type | Description | +|--------|------|-------------| +| `setLimit(Int)` | Int | Number of call logs to fetch per request (default: 30, max: 100) | +| `setSessionType(String)` | String | Filter by call type: `video` or `audio` | +| `setCallStatus(String)` | String | Filter by call status | +| `setHasRecording(Bool)` | Bool | Filter calls that have recordings | +| `setCallCategory(String)` | String | Filter by category: `call` or `meet` | +| `setCallDirection(String)` | String | Filter by direction: `incoming` or `outgoing` | +| `setUid(String)` | String | Filter calls with a specific user | +| `setGuid(String)` | String | Filter calls with a specific group | + +### Filter Examples + + + +```swift +// Fetch only video calls +let videoCallsRequest = CallLogsRequest.CallLogsRequestBuilder() + .setSessionType("video") + .setLimit(20) + .build() + +// Fetch calls with recordings +let recordedCallsRequest = CallLogsRequest.CallLogsRequestBuilder() + .setHasRecording(true) + .build() + +// Fetch missed incoming calls +let missedCallsRequest = CallLogsRequest.CallLogsRequestBuilder() + .setCallStatus("missed") + .setCallDirection("incoming") + .build() + +// Fetch calls with a specific user +let userCallsRequest = CallLogsRequest.CallLogsRequestBuilder() + .setUid("user_id") + .build() +``` + + +```objectivec +// Fetch only video calls +CallLogsRequest *videoCallsRequest = [[[[CallLogsRequest CallLogsRequestBuilder] + setSessionType:@"video"] + setLimit:20] + build]; + +// Fetch calls with recordings +CallLogsRequest *recordedCallsRequest = [[[CallLogsRequest CallLogsRequestBuilder] + setHasRecording:YES] + build]; + +// Fetch missed incoming calls +CallLogsRequest *missedCallsRequest = [[[[CallLogsRequest CallLogsRequestBuilder] + setCallStatus:@"missed"] + setCallDirection:@"incoming"] + build]; + +// Fetch calls with a specific user +CallLogsRequest *userCallsRequest = [[[CallLogsRequest CallLogsRequestBuilder] + setUid:@"user_id"] + build]; +``` + + + +## Pagination + +Use `fetchNext()` and `fetchPrevious()` for pagination: + + + +```swift +// Fetch next page +callLogRequest.fetchNext(onSuccess: { callLogs in + // Handle next page +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) + +// Fetch previous page +callLogRequest.fetchPrevious(onSuccess: { callLogs in + // Handle previous page +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + + +```objectivec +// Fetch next page +[callLogRequest fetchNextOnSuccess:^(NSArray * callLogs) { + // Handle next page +} onError:^(CometChatCallException * error) { + NSLog(@"Error: %@", error.errorDescription); +}]; + +// Fetch previous page +[callLogRequest fetchPreviousOnSuccess:^(NSArray * callLogs) { + // Handle previous page +} onError:^(CometChatCallException * error) { + NSLog(@"Error: %@", error.errorDescription); +}]; +``` + + + +## CallLog Object + +Each `CallLog` object contains detailed information about a call: + +| Property | Type | Description | +|----------|------|-------------| +| `sessionID` | String | Unique identifier for the call session | +| `initiator` | CallEntity | User who initiated the call | +| `receiver` | CallEntity | User or group that received the call | +| `receiverType` | String | `user` or `group` | +| `type` | String | Call type: `video` or `audio` | +| `status` | String | Final status of the call | +| `callCategory` | String | Category: `call` or `meet` | +| `initiatedAt` | Int | Timestamp when call was initiated | +| `endedAt` | Int | Timestamp when call ended | +| `totalDuration` | String | Human-readable duration (e.g., "5:30") | +| `totalDurationInMinutes` | Double | Duration in minutes | +| `totalAudioMinutes` | Double | Audio duration in minutes | +| `totalVideoMinutes` | Double | Video duration in minutes | +| `totalParticipants` | Int | Number of participants | +| `hasRecording` | Bool | Whether the call was recorded | +| `recordings` | [Recording] | List of recording objects | + +## Access Recordings + +If a call has recordings, access them through the `recordings` property: + + + +```swift +callLogRequest.fetchNext(onSuccess: { callLogs in + for callLog in callLogs { + if callLog.hasRecording { + for recording in callLog.recordings ?? [] { + print("Recording ID: \(recording.rid ?? "")") + print("Recording URL: \(recording.recordingURL ?? "")") + print("Duration: \(recording.duration) seconds") + } + } + } +}, onError: { error in + print("Error: \(error?.errorDescription ?? "")") +}) +``` + + +```objectivec +[callLogRequest fetchNextOnSuccess:^(NSArray * callLogs) { + for (CallLog *callLog in callLogs) { + if (callLog.hasRecording) { + for (Recording *recording in callLog.recordings) { + NSLog(@"Recording ID: %@", recording.rid); + NSLog(@"Recording URL: %@", recording.recordingURL); + NSLog(@"Duration: %f seconds", recording.duration); + } + } + } +} onError:^(CometChatCallException * error) { + NSLog(@"Error: %@", error.errorDescription); +}]; +``` + + + + +| Status | Description | +|--------|-------------| +| `ongoing` | Call is currently in progress | +| `busy` | Receiver was busy | +| `rejected` | Call was rejected | +| `cancelled` | Call was cancelled by initiator | +| `ended` | Call ended normally | +| `missed` | Call was missed | +| `initiated` | Call was initiated but not answered | +| `unanswered` | Call was not answered | + + + +| Category | Description | +|----------|-------------| +| `call` | Direct call between users | +| `meet` | Meeting/conference call | + + + +| Direction | Description | +|-----------|-------------| +| `incoming` | Call received by the user | +| `outgoing` | Call initiated by the user | + diff --git a/calls/ios/custom-control-panel.mdx b/calls/ios/custom-control-panel.mdx new file mode 100644 index 00000000..856c9d83 --- /dev/null +++ b/calls/ios/custom-control-panel.mdx @@ -0,0 +1,454 @@ +--- +title: "Custom Control Panel" +sidebarTitle: "Custom Control Panel" +--- + +Build a fully customized control panel for your call interface by hiding the default controls and implementing your own UI with call actions. This guide walks you through creating a custom control panel with essential call controls. + +## Overview + +Custom control panels allow you to: +- Match your app's branding and design language +- Simplify the interface by showing only relevant controls +- Add custom functionality and workflows +- Create unique user experiences + +This guide demonstrates building a basic custom control panel with: +- Mute/Unmute audio button +- Pause/Resume video button +- Switch camera button +- End call button + +## Prerequisites + +- CometChat Calls SDK installed and initialized +- Active call session (see [Join Session](/calls/ios/join-session)) +- Familiarity with [Actions](/calls/ios/actions) and [Events](/calls/ios/events) + +--- + +## Step 1: Hide Default Controls + +Configure your session settings to hide the default control panel: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .hideControlPanel(true) + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + hideControlPanel:YES] + build]; +``` + + + + +You can also hide individual buttons while keeping the control panel visible. See [SessionSettingsBuilder](/calls/ios/session-settings) for all options. + + +--- + +## Step 2: Create Custom Layout + +Create a custom view for your controls programmatically or in Interface Builder: + + + +```swift +class CallViewController: UIViewController { + + // Call container view + private let callContainer = UIView() + + // Custom control panel + private let controlPanel = UIStackView() + private let btnToggleAudio = UIButton(type: .system) + private let btnToggleVideo = UIButton(type: .system) + private let btnSwitchCamera = UIButton(type: .system) + private let btnEndCall = UIButton(type: .system) + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupControlListeners() + } + + private func setupUI() { + view.backgroundColor = .black + + // Setup call container + callContainer.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(callContainer) + + // Setup control panel + controlPanel.axis = .horizontal + controlPanel.distribution = .equalSpacing + controlPanel.alignment = .center + controlPanel.spacing = 20 + controlPanel.translatesAutoresizingMaskIntoConstraints = false + controlPanel.backgroundColor = UIColor.black.withAlphaComponent(0.8) + controlPanel.layoutMargins = UIEdgeInsets(top: 16, left: 32, bottom: 16, right: 32) + controlPanel.isLayoutMarginsRelativeArrangement = true + view.addSubview(controlPanel) + + // Configure buttons + configureButton(btnToggleAudio, imageName: "mic.fill", backgroundColor: .darkGray) + configureButton(btnToggleVideo, imageName: "video.fill", backgroundColor: .darkGray) + configureButton(btnSwitchCamera, imageName: "camera.rotate.fill", backgroundColor: .darkGray) + configureButton(btnEndCall, imageName: "phone.down.fill", backgroundColor: .systemRed) + + // Add buttons to control panel + controlPanel.addArrangedSubview(btnToggleAudio) + controlPanel.addArrangedSubview(btnToggleVideo) + controlPanel.addArrangedSubview(btnSwitchCamera) + controlPanel.addArrangedSubview(btnEndCall) + + // Layout constraints + NSLayoutConstraint.activate([ + callContainer.topAnchor.constraint(equalTo: view.topAnchor), + callContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), + callContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor), + callContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor), + + controlPanel.leadingAnchor.constraint(equalTo: view.leadingAnchor), + controlPanel.trailingAnchor.constraint(equalTo: view.trailingAnchor), + controlPanel.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + controlPanel.heightAnchor.constraint(equalToConstant: 80) + ]) + } + + private func configureButton(_ button: UIButton, imageName: String, backgroundColor: UIColor) { + button.setImage(UIImage(systemName: imageName), for: .normal) + button.tintColor = .white + button.backgroundColor = backgroundColor + button.layer.cornerRadius = 28 + button.translatesAutoresizingMaskIntoConstraints = false + NSLayoutConstraint.activate([ + button.widthAnchor.constraint(equalToConstant: 56), + button.heightAnchor.constraint(equalToConstant: 56) + ]) + } +} +``` + + +```objectivec +@interface CallViewController () +@property (nonatomic, strong) UIView *callContainer; +@property (nonatomic, strong) UIStackView *controlPanel; +@property (nonatomic, strong) UIButton *btnToggleAudio; +@property (nonatomic, strong) UIButton *btnToggleVideo; +@property (nonatomic, strong) UIButton *btnSwitchCamera; +@property (nonatomic, strong) UIButton *btnEndCall; +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [self setupUI]; + [self setupControlListeners]; +} + +- (void)setupUI { + self.view.backgroundColor = [UIColor blackColor]; + + // Setup call container + self.callContainer = [[UIView alloc] init]; + self.callContainer.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.callContainer]; + + // Setup control panel + self.controlPanel = [[UIStackView alloc] init]; + self.controlPanel.axis = UILayoutConstraintAxisHorizontal; + self.controlPanel.distribution = UIStackViewDistributionEqualSpacing; + self.controlPanel.alignment = UIStackViewAlignmentCenter; + self.controlPanel.spacing = 20; + self.controlPanel.translatesAutoresizingMaskIntoConstraints = NO; + self.controlPanel.backgroundColor = [[UIColor blackColor] colorWithAlphaComponent:0.8]; + [self.view addSubview:self.controlPanel]; + + // Configure and add buttons + self.btnToggleAudio = [self createButtonWithImageName:@"mic.fill" backgroundColor:[UIColor darkGrayColor]]; + self.btnToggleVideo = [self createButtonWithImageName:@"video.fill" backgroundColor:[UIColor darkGrayColor]]; + self.btnSwitchCamera = [self createButtonWithImageName:@"camera.rotate.fill" backgroundColor:[UIColor darkGrayColor]]; + self.btnEndCall = [self createButtonWithImageName:@"phone.down.fill" backgroundColor:[UIColor systemRedColor]]; + + [self.controlPanel addArrangedSubview:self.btnToggleAudio]; + [self.controlPanel addArrangedSubview:self.btnToggleVideo]; + [self.controlPanel addArrangedSubview:self.btnSwitchCamera]; + [self.controlPanel addArrangedSubview:self.btnEndCall]; + + // Layout constraints + [NSLayoutConstraint activateConstraints:@[ + [self.callContainer.topAnchor constraintEqualToAnchor:self.view.topAnchor], + [self.callContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [self.callContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [self.callContainer.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor], + + [self.controlPanel.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [self.controlPanel.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [self.controlPanel.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor], + [self.controlPanel.heightAnchor constraintEqualToConstant:80] + ]]; +} + +- (UIButton *)createButtonWithImageName:(NSString *)imageName backgroundColor:(UIColor *)backgroundColor { + UIButton *button = [UIButton buttonWithType:UIButtonTypeSystem]; + [button setImage:[UIImage systemImageNamed:imageName] forState:UIControlStateNormal]; + button.tintColor = [UIColor whiteColor]; + button.backgroundColor = backgroundColor; + button.layer.cornerRadius = 28; + button.translatesAutoresizingMaskIntoConstraints = NO; + [NSLayoutConstraint activateConstraints:@[ + [button.widthAnchor constraintEqualToConstant:56], + [button.heightAnchor constraintEqualToConstant:56] + ]]; + return button; +} + +@end +``` + + + +--- + +## Step 3: Implement Control Actions + +Set up button actions and call the appropriate SDK methods: + + + +```swift +private var isAudioMuted = false +private var isVideoPaused = false + +private func setupControlListeners() { + btnToggleAudio.addTarget(self, action: #selector(toggleAudio), for: .touchUpInside) + btnToggleVideo.addTarget(self, action: #selector(toggleVideo), for: .touchUpInside) + btnSwitchCamera.addTarget(self, action: #selector(switchCamera), for: .touchUpInside) + btnEndCall.addTarget(self, action: #selector(endCall), for: .touchUpInside) +} + +@objc private func toggleAudio() { + if isAudioMuted { + CallSession.shared.unMuteAudio() + } else { + CallSession.shared.muteAudio() + } +} + +@objc private func toggleVideo() { + if isVideoPaused { + CallSession.shared.resumeVideo() + } else { + CallSession.shared.pauseVideo() + } +} + +@objc private func switchCamera() { + CallSession.shared.switchCamera() +} + +@objc private func endCall() { + CallSession.shared.leaveSession() + navigationController?.popViewController(animated: true) +} +``` + + +```objectivec +@interface CallViewController () +@property (nonatomic, assign) BOOL isAudioMuted; +@property (nonatomic, assign) BOOL isVideoPaused; +@end + +- (void)setupControlListeners { + [self.btnToggleAudio addTarget:self action:@selector(toggleAudio) forControlEvents:UIControlEventTouchUpInside]; + [self.btnToggleVideo addTarget:self action:@selector(toggleVideo) forControlEvents:UIControlEventTouchUpInside]; + [self.btnSwitchCamera addTarget:self action:@selector(switchCamera) forControlEvents:UIControlEventTouchUpInside]; + [self.btnEndCall addTarget:self action:@selector(endCall) forControlEvents:UIControlEventTouchUpInside]; +} + +- (void)toggleAudio { + if (self.isAudioMuted) { + [[CallSession shared] unMuteAudio]; + } else { + [[CallSession shared] muteAudio]; + } +} + +- (void)toggleVideo { + if (self.isVideoPaused) { + [[CallSession shared] resumeVideo]; + } else { + [[CallSession shared] pauseVideo]; + } +} + +- (void)switchCamera { + [[CallSession shared] switchCamera]; +} + +- (void)endCall { + [[CallSession shared] leaveSession]; + [self.navigationController popViewControllerAnimated:YES]; +} +``` + + + +--- + +## Step 4: Handle State Updates + +Use `MediaEventsListener` to keep your UI synchronized with the actual call state: + + + +```swift +extension CallViewController: MediaEventsListener { + + override func viewDidLoad() { + super.viewDidLoad() + // ... other setup + CallSession.shared.addMediaEventsListener(self) + } + + deinit { + CallSession.shared.removeMediaEventsListener(self) + } + + func onAudioMuted() { + DispatchQueue.main.async { + self.isAudioMuted = true + self.btnToggleAudio.setImage(UIImage(systemName: "mic.slash.fill"), for: .normal) + } + } + + func onAudioUnMuted() { + DispatchQueue.main.async { + self.isAudioMuted = false + self.btnToggleAudio.setImage(UIImage(systemName: "mic.fill"), for: .normal) + } + } + + func onVideoPaused() { + DispatchQueue.main.async { + self.isVideoPaused = true + self.btnToggleVideo.setImage(UIImage(systemName: "video.slash.fill"), for: .normal) + } + } + + func onVideoResumed() { + DispatchQueue.main.async { + self.isVideoPaused = false + self.btnToggleVideo.setImage(UIImage(systemName: "video.fill"), for: .normal) + } + } + + // Other MediaEventsListener callbacks + func onRecordingStarted() {} + func onRecordingStopped() {} + func onScreenShareStarted() {} + func onScreenShareStopped() {} + func onAudioModeChanged(audioModeType: AudioModeType) {} + func onCameraFacingChanged(cameraFacing: CameraFacing) {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +- (void)viewDidLoad { + [super viewDidLoad]; + // ... other setup + [[CallSession shared] addMediaEventsListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeMediaEventsListener:self]; +} + +- (void)onAudioMuted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.isAudioMuted = YES; + [self.btnToggleAudio setImage:[UIImage systemImageNamed:@"mic.slash.fill"] forState:UIControlStateNormal]; + }); +} + +- (void)onAudioUnMuted { + dispatch_async(dispatch_get_main_queue(), ^{ + self.isAudioMuted = NO; + [self.btnToggleAudio setImage:[UIImage systemImageNamed:@"mic.fill"] forState:UIControlStateNormal]; + }); +} + +- (void)onVideoPaused { + dispatch_async(dispatch_get_main_queue(), ^{ + self.isVideoPaused = YES; + [self.btnToggleVideo setImage:[UIImage systemImageNamed:@"video.slash.fill"] forState:UIControlStateNormal]; + }); +} + +- (void)onVideoResumed { + dispatch_async(dispatch_get_main_queue(), ^{ + self.isVideoPaused = NO; + [self.btnToggleVideo setImage:[UIImage systemImageNamed:@"video.fill"] forState:UIControlStateNormal]; + }); +} +``` + + + +Use `SessionStatusListener` to handle session end events: + + + +```swift +extension CallViewController: SessionStatusListener { + + func onSessionLeft() { + DispatchQueue.main.async { + self.navigationController?.popViewController(animated: true) + } + } + + func onConnectionClosed() { + DispatchQueue.main.async { + self.navigationController?.popViewController(animated: true) + } + } + + func onSessionJoined() {} + func onSessionTimedOut() {} + func onConnectionLost() {} + func onConnectionRestored() {} +} +``` + + +```objectivec +- (void)onSessionLeft { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.navigationController popViewControllerAnimated:YES]; + }); +} + +- (void)onConnectionClosed { + dispatch_async(dispatch_get_main_queue(), ^{ + [self.navigationController popViewControllerAnimated:YES]; + }); +} +``` + + diff --git a/calls/ios/custom-participant-list.mdx b/calls/ios/custom-participant-list.mdx new file mode 100644 index 00000000..bad35682 --- /dev/null +++ b/calls/ios/custom-participant-list.mdx @@ -0,0 +1,647 @@ +--- +title: "Custom Participant List" +sidebarTitle: "Custom Participant List" +--- + +Build a custom participant list UI that displays real-time participant information with full control over layout and interactions. This guide demonstrates how to hide the default participant list and create your own using participant events and actions. + +## Overview + +The SDK provides participant data through events, allowing you to build custom UIs for: +- Participant roster with search and filtering +- Custom participant cards with role badges or metadata +- Moderation dashboards with quick access to controls +- Attendance tracking and engagement monitoring + +## Prerequisites + +- CometChat Calls SDK installed and initialized +- Active call session (see [Join Session](/calls/ios/join-session)) +- Basic understanding of UITableView or UICollectionView + +--- + +## Step 1: Hide Default Participant List + +Configure session settings to hide the default participant list button: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .hideParticipantListButton(true) + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + hideParticipantListButton:YES] + build]; +``` + + + +--- + +## Step 2: Create Participant List Layout + +Create a custom view controller for displaying participants: + + + +```swift +class ParticipantListViewController: UIViewController { + + private let tableView = UITableView() + private let searchBar = UISearchBar() + private var participants: [Participant] = [] + private var filteredParticipants: [Participant] = [] + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + setupParticipantListener() + } + + private func setupUI() { + title = "Participants" + view.backgroundColor = .systemBackground + + // Setup search bar + searchBar.placeholder = "Search participants..." + searchBar.delegate = self + searchBar.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(searchBar) + + // Setup table view + tableView.delegate = self + tableView.dataSource = self + tableView.register(ParticipantCell.self, forCellReuseIdentifier: "ParticipantCell") + tableView.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(tableView) + + // Layout constraints + NSLayoutConstraint.activate([ + searchBar.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + searchBar.leadingAnchor.constraint(equalTo: view.leadingAnchor), + searchBar.trailingAnchor.constraint(equalTo: view.trailingAnchor), + + tableView.topAnchor.constraint(equalTo: searchBar.bottomAnchor), + tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + + // Add close button + navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .close, + target: self, + action: #selector(dismissView) + ) + } + + @objc private func dismissView() { + dismiss(animated: true) + } +} +``` + + +```objectivec +@interface ParticipantListViewController () +@property (nonatomic, strong) UITableView *tableView; +@property (nonatomic, strong) UISearchBar *searchBar; +@property (nonatomic, strong) NSArray *participants; +@property (nonatomic, strong) NSArray *filteredParticipants; +@end + +@implementation ParticipantListViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [self setupUI]; + [self setupParticipantListener]; +} + +- (void)setupUI { + self.title = @"Participants"; + self.view.backgroundColor = [UIColor systemBackgroundColor]; + + // Setup search bar + self.searchBar = [[UISearchBar alloc] init]; + self.searchBar.placeholder = @"Search participants..."; + self.searchBar.delegate = self; + self.searchBar.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.searchBar]; + + // Setup table view + self.tableView = [[UITableView alloc] init]; + self.tableView.delegate = self; + self.tableView.dataSource = self; + [self.tableView registerClass:[ParticipantCell class] forCellReuseIdentifier:@"ParticipantCell"]; + self.tableView.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.tableView]; + + // Layout constraints + [NSLayoutConstraint activateConstraints:@[ + [self.searchBar.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor], + [self.searchBar.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [self.searchBar.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + + [self.tableView.topAnchor constraintEqualToAnchor:self.searchBar.bottomAnchor], + [self.tableView.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [self.tableView.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [self.tableView.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor] + ]]; + + // Add close button + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem:UIBarButtonSystemItemClose + target:self + action:@selector(dismissView)]; +} + +- (void)dismissView { + [self dismissViewControllerAnimated:YES completion:nil]; +} + +@end +``` + + + +--- + +## Step 3: Create Participant Cell + +Build a custom table view cell to display participant information: + + + +```swift +class ParticipantCell: UITableViewCell { + + private let avatarImageView = UIImageView() + private let nameLabel = UILabel() + private let statusLabel = UILabel() + private let muteButton = UIButton(type: .system) + private let pinButton = UIButton(type: .system) + + var participant: Participant? + var onMuteAction: ((Participant) -> Void)? + var onPinAction: ((Participant) -> Void)? + + override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { + super.init(style: style, reuseIdentifier: reuseIdentifier) + setupUI() + } + + required init?(coder: NSCoder) { + fatalError("init(coder:) has not been implemented") + } + + private func setupUI() { + // Avatar + avatarImageView.layer.cornerRadius = 20 + avatarImageView.clipsToBounds = true + avatarImageView.backgroundColor = .systemGray4 + avatarImageView.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(avatarImageView) + + // Name label + nameLabel.font = .systemFont(ofSize: 16, weight: .semibold) + nameLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(nameLabel) + + // Status label + statusLabel.font = .systemFont(ofSize: 12) + statusLabel.textColor = .secondaryLabel + statusLabel.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(statusLabel) + + // Action buttons + muteButton.setImage(UIImage(systemName: "mic.slash"), for: .normal) + muteButton.addTarget(self, action: #selector(muteButtonTapped), for: .touchUpInside) + muteButton.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(muteButton) + + pinButton.setImage(UIImage(systemName: "pin"), for: .normal) + pinButton.addTarget(self, action: #selector(pinButtonTapped), for: .touchUpInside) + pinButton.translatesAutoresizingMaskIntoConstraints = false + contentView.addSubview(pinButton) + + // Layout + NSLayoutConstraint.activate([ + avatarImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 16), + avatarImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + avatarImageView.widthAnchor.constraint(equalToConstant: 40), + avatarImageView.heightAnchor.constraint(equalToConstant: 40), + + nameLabel.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 12), + nameLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 12), + + statusLabel.leadingAnchor.constraint(equalTo: nameLabel.leadingAnchor), + statusLabel.topAnchor.constraint(equalTo: nameLabel.bottomAnchor, constant: 4), + statusLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor, constant: -12), + + pinButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -16), + pinButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + pinButton.widthAnchor.constraint(equalToConstant: 32), + + muteButton.trailingAnchor.constraint(equalTo: pinButton.leadingAnchor, constant: -8), + muteButton.centerYAnchor.constraint(equalTo: contentView.centerYAnchor), + muteButton.widthAnchor.constraint(equalToConstant: 32) + ]) + } + + func configure(with participant: Participant) { + self.participant = participant + nameLabel.text = participant.name + + // Build status text + var statusParts: [String] = [] + if participant.isAudioMuted { statusParts.append("🔇 Muted") } + if participant.isVideoPaused { statusParts.append("📹 Video Off") } + if participant.isPresenting { statusParts.append("🖥️ Presenting") } + if participant.raisedHandTimestamp > 0 { statusParts.append("✋ Hand Raised") } + if participant.isPinned { statusParts.append("📌 Pinned") } + + statusLabel.text = statusParts.isEmpty ? "Active" : statusParts.joined(separator: " • ") + + // Update button states + muteButton.alpha = participant.isAudioMuted ? 0.5 : 1.0 + pinButton.tintColor = participant.isPinned ? .systemBlue : .systemGray + } + + @objc private func muteButtonTapped() { + guard let participant = participant else { return } + onMuteAction?(participant) + } + + @objc private func pinButtonTapped() { + guard let participant = participant else { return } + onPinAction?(participant) + } +} +``` + + +```objectivec +@interface ParticipantCell : UITableViewCell +@property (nonatomic, strong) Participant *participant; +@property (nonatomic, copy) void (^onMuteAction)(Participant *); +@property (nonatomic, copy) void (^onPinAction)(Participant *); +- (void)configureWithParticipant:(Participant *)participant; +@end + +@implementation ParticipantCell { + UIImageView *_avatarImageView; + UILabel *_nameLabel; + UILabel *_statusLabel; + UIButton *_muteButton; + UIButton *_pinButton; +} + +- (instancetype)initWithStyle:(UITableViewCellStyle)style reuseIdentifier:(NSString *)reuseIdentifier { + self = [super initWithStyle:style reuseIdentifier:reuseIdentifier]; + if (self) { + [self setupUI]; + } + return self; +} + +- (void)setupUI { + // Avatar + _avatarImageView = [[UIImageView alloc] init]; + _avatarImageView.layer.cornerRadius = 20; + _avatarImageView.clipsToBounds = YES; + _avatarImageView.backgroundColor = [UIColor systemGray4Color]; + _avatarImageView.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentView addSubview:_avatarImageView]; + + // Name label + _nameLabel = [[UILabel alloc] init]; + _nameLabel.font = [UIFont systemFontOfSize:16 weight:UIFontWeightSemibold]; + _nameLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentView addSubview:_nameLabel]; + + // Status label + _statusLabel = [[UILabel alloc] init]; + _statusLabel.font = [UIFont systemFontOfSize:12]; + _statusLabel.textColor = [UIColor secondaryLabelColor]; + _statusLabel.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentView addSubview:_statusLabel]; + + // Action buttons + _muteButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [_muteButton setImage:[UIImage systemImageNamed:@"mic.slash"] forState:UIControlStateNormal]; + [_muteButton addTarget:self action:@selector(muteButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + _muteButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentView addSubview:_muteButton]; + + _pinButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [_pinButton setImage:[UIImage systemImageNamed:@"pin"] forState:UIControlStateNormal]; + [_pinButton addTarget:self action:@selector(pinButtonTapped) forControlEvents:UIControlEventTouchUpInside]; + _pinButton.translatesAutoresizingMaskIntoConstraints = NO; + [self.contentView addSubview:_pinButton]; + + // Layout constraints + [NSLayoutConstraint activateConstraints:@[ + [_avatarImageView.leadingAnchor constraintEqualToAnchor:self.contentView.leadingAnchor constant:16], + [_avatarImageView.centerYAnchor constraintEqualToAnchor:self.contentView.centerYAnchor], + [_avatarImageView.widthAnchor constraintEqualToConstant:40], + [_avatarImageView.heightAnchor constraintEqualToConstant:40], + + [_nameLabel.leadingAnchor constraintEqualToAnchor:_avatarImageView.trailingAnchor constant:12], + [_nameLabel.topAnchor constraintEqualToAnchor:self.contentView.topAnchor constant:12], + + [_statusLabel.leadingAnchor constraintEqualToAnchor:_nameLabel.leadingAnchor], + [_statusLabel.topAnchor constraintEqualToAnchor:_nameLabel.bottomAnchor constant:4], + [_statusLabel.bottomAnchor constraintEqualToAnchor:self.contentView.bottomAnchor constant:-12], + + [_pinButton.trailingAnchor constraintEqualToAnchor:self.contentView.trailingAnchor constant:-16], + [_pinButton.centerYAnchor constraintEqualToAnchor:self.contentView.centerYAnchor], + [_pinButton.widthAnchor constraintEqualToConstant:32], + + [_muteButton.trailingAnchor constraintEqualToAnchor:_pinButton.leadingAnchor constant:-8], + [_muteButton.centerYAnchor constraintEqualToAnchor:self.contentView.centerYAnchor], + [_muteButton.widthAnchor constraintEqualToConstant:32] + ]]; +} + +- (void)configureWithParticipant:(Participant *)participant { + self.participant = participant; + _nameLabel.text = participant.name; + + // Build status text + NSMutableArray *statusParts = [NSMutableArray array]; + if (participant.isAudioMuted) [statusParts addObject:@"🔇 Muted"]; + if (participant.isVideoPaused) [statusParts addObject:@"📹 Video Off"]; + if (participant.isPresenting) [statusParts addObject:@"🖥️ Presenting"]; + if (participant.raisedHandTimestamp > 0) [statusParts addObject:@"✋ Hand Raised"]; + if (participant.isPinned) [statusParts addObject:@"📌 Pinned"]; + + _statusLabel.text = statusParts.count == 0 ? @"Active" : [statusParts componentsJoinedByString:@" • "]; + + // Update button states + _muteButton.alpha = participant.isAudioMuted ? 0.5 : 1.0; + _pinButton.tintColor = participant.isPinned ? [UIColor systemBlueColor] : [UIColor systemGrayColor]; +} + +- (void)muteButtonTapped { + if (self.onMuteAction && self.participant) { + self.onMuteAction(self.participant); + } +} + +- (void)pinButtonTapped { + if (self.onPinAction && self.participant) { + self.onPinAction(self.participant); + } +} + +@end +``` + + + +--- + +## Step 4: Implement Participant Events + +Listen for participant updates and handle actions: + + + +```swift +extension ParticipantListViewController: ParticipantEventListener { + + private func setupParticipantListener() { + CallSession.shared.addParticipantEventListener(self) + } + + deinit { + CallSession.shared.removeParticipantEventListener(self) + } + + func onParticipantListChanged(participants: [Participant]) { + DispatchQueue.main.async { + self.participants = participants + self.filteredParticipants = participants + self.title = "Participants (\(participants.count))" + self.tableView.reloadData() + } + } + + func onParticipantJoined(participant: Participant) { + print("\(participant.name) joined") + } + + func onParticipantLeft(participant: Participant) { + print("\(participant.name) left") + } + + func onParticipantAudioMuted(participant: Participant) { + // Table will update via onParticipantListChanged + } + + func onParticipantAudioUnmuted(participant: Participant) {} + func onParticipantVideoPaused(participant: Participant) {} + func onParticipantVideoResumed(participant: Participant) {} + func onParticipantHandRaised(participant: Participant) {} + func onParticipantHandLowered(participant: Participant) {} +} +``` + + +```objectivec +@interface ParticipantListViewController () +@end + +- (void)setupParticipantListener { + [[CallSession shared] addParticipantEventListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeParticipantEventListener:self]; +} + +- (void)onParticipantListChangedWithParticipants:(NSArray *)participants { + dispatch_async(dispatch_get_main_queue(), ^{ + self.participants = participants; + self.filteredParticipants = participants; + self.title = [NSString stringWithFormat:@"Participants (%lu)", (unsigned long)participants.count]; + [self.tableView reloadData]; + }); +} + +- (void)onParticipantJoinedWithParticipant:(Participant *)participant { + NSLog(@"%@ joined", participant.name); +} + +- (void)onParticipantLeftWithParticipant:(Participant *)participant { + NSLog(@"%@ left", participant.name); +} +``` + + + +--- + +## Step 5: Implement Table View Data Source + + + +```swift +extension ParticipantListViewController: UITableViewDelegate, UITableViewDataSource { + + func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return filteredParticipants.count + } + + func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "ParticipantCell", for: indexPath) as! ParticipantCell + let participant = filteredParticipants[indexPath.row] + + cell.configure(with: participant) + + cell.onMuteAction = { [weak self] participant in + CallSession.shared.muteParticipant(participant.uid) + } + + cell.onPinAction = { [weak self] participant in + if participant.isPinned { + CallSession.shared.unPinParticipant() + } else { + CallSession.shared.pinParticipant(participant.uid) + } + } + + return cell + } +} + +extension ParticipantListViewController: UISearchBarDelegate { + + func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) { + if searchText.isEmpty { + filteredParticipants = participants + } else { + filteredParticipants = participants.filter { + $0.name.localizedCaseInsensitiveContains(searchText) + } + } + tableView.reloadData() + } +} +``` + + +```objectivec +- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section { + return self.filteredParticipants.count; +} + +- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath { + ParticipantCell *cell = [tableView dequeueReusableCellWithIdentifier:@"ParticipantCell" forIndexPath:indexPath]; + Participant *participant = self.filteredParticipants[indexPath.row]; + + [cell configureWithParticipant:participant]; + + __weak typeof(self) weakSelf = self; + cell.onMuteAction = ^(Participant *p) { + [[CallSession shared] muteParticipant:p.uid]; + }; + + cell.onPinAction = ^(Participant *p) { + if (p.isPinned) { + [[CallSession shared] unPinParticipant]; + } else { + [[CallSession shared] pinParticipant:p.uid]; + } + }; + + return cell; +} + +- (void)searchBar:(UISearchBar *)searchBar textDidChange:(NSString *)searchText { + if (searchText.length == 0) { + self.filteredParticipants = self.participants; + } else { + NSPredicate *predicate = [NSPredicate predicateWithFormat:@"name CONTAINS[cd] %@", searchText]; + self.filteredParticipants = [self.participants filteredArrayUsingPredicate:predicate]; + } + [self.tableView reloadData]; +} +``` + + + +--- + +## Step 6: Present Participant List + +Show the participant list from your call view controller: + + + +```swift +class CallViewController: UIViewController { + + private let participantListButton = UIButton(type: .system) + + private func setupParticipantListButton() { + participantListButton.setImage(UIImage(systemName: "person.3"), for: .normal) + participantListButton.addTarget(self, action: #selector(showParticipantList), for: .touchUpInside) + // Add to your view hierarchy + } + + @objc private func showParticipantList() { + let participantListVC = ParticipantListViewController() + let navController = UINavigationController(rootViewController: participantListVC) + navController.modalPresentationStyle = .pageSheet + + if let sheet = navController.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true + } + + present(navController, animated: true) + } +} +``` + + +```objectivec +- (void)setupParticipantListButton { + self.participantListButton = [UIButton buttonWithType:UIButtonTypeSystem]; + [self.participantListButton setImage:[UIImage systemImageNamed:@"person.3"] forState:UIControlStateNormal]; + [self.participantListButton addTarget:self action:@selector(showParticipantList) forControlEvents:UIControlEventTouchUpInside]; + // Add to your view hierarchy +} + +- (void)showParticipantList { + ParticipantListViewController *participantListVC = [[ParticipantListViewController alloc] init]; + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:participantListVC]; + navController.modalPresentationStyle = UIModalPresentationPageSheet; + + UISheetPresentationController *sheet = navController.sheetPresentationController; + if (sheet) { + sheet.detents = @[UISheetPresentationControllerDetent.mediumDetent, UISheetPresentationControllerDetent.largeDetent]; + sheet.prefersGrabberVisible = YES; + } + + [self presentViewController:navController animated:YES completion:nil]; +} +``` + + + +--- + +## Related Documentation + +- [Participant Management](/calls/ios/participant-management) - Participant actions and events +- [Events](/calls/ios/events) - All available event listeners +- [Actions](/calls/ios/actions) - Available call actions diff --git a/calls/ios/events.mdx b/calls/ios/events.mdx new file mode 100644 index 00000000..eadd2abb --- /dev/null +++ b/calls/ios/events.mdx @@ -0,0 +1,585 @@ +--- +title: "Events" +sidebarTitle: "Events" +--- + +Handle call session events to build responsive UIs. The SDK provides five event listener protocols to monitor session status, participant activities, media changes, button clicks, and layout changes. + +## Get CallSession Instance + +The `CallSession` is a singleton that manages the active call. All event listener registrations and session control methods are accessed through this instance. + + + +```swift +let callSession = CallSession.shared +``` + + +```objectivec +CallSession *callSession = [CallSession shared]; +``` + + + + +Remember to remove listeners when your view controller is deallocated to prevent memory leaks. Use `removeSessionStatusListener`, `removeParticipantEventListener`, etc. + + +--- + +## Session Events + +Monitor the call session lifecycle including join/leave events and connection status. + + + +```swift +class CallViewController: UIViewController, SessionStatusListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addSessionStatusListener(self) + } + + deinit { + CallSession.shared.removeSessionStatusListener(self) + } + + func onSessionJoined() { + // Successfully connected to the session + } + + func onSessionLeft() { + // Left the session + navigationController?.popViewController(animated: true) + } + + func onSessionTimedOut() { + // Session ended due to inactivity + navigationController?.popViewController(animated: true) + } + + func onConnectionLost() { + // Network interrupted, attempting reconnection + } + + func onConnectionRestored() { + // Connection restored after being lost + } + + func onConnectionClosed() { + // Connection permanently closed + navigationController?.popViewController(animated: true) + } +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addSessionStatusListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeSessionStatusListener:self]; +} + +- (void)onSessionJoined { + // Successfully connected to the session +} + +- (void)onSessionLeft { + // Left the session + [self.navigationController popViewControllerAnimated:YES]; +} + +- (void)onSessionTimedOut { + // Session ended due to inactivity + [self.navigationController popViewControllerAnimated:YES]; +} + +- (void)onConnectionLost { + // Network interrupted, attempting reconnection +} + +- (void)onConnectionRestored { + // Connection restored after being lost +} + +- (void)onConnectionClosed { + // Connection permanently closed + [self.navigationController popViewControllerAnimated:YES]; +} + +@end +``` + + + +| Event | Description | +|-------|-------------| +| `onSessionJoined()` | Successfully connected and joined the session | +| `onSessionLeft()` | Left the session via `leaveSession()` or was removed | +| `onSessionTimedOut()` | Session ended due to inactivity timeout | +| `onConnectionLost()` | Network interrupted, SDK attempting reconnection | +| `onConnectionRestored()` | Connection restored after being lost | +| `onConnectionClosed()` | Connection permanently closed, cannot reconnect | + +--- + +## Participant Events + +Monitor participant activities including join/leave, audio/video state, hand raise, screen sharing, and recording. + + + +```swift +class CallViewController: UIViewController, ParticipantEventListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addParticipantEventListener(self) + } + + deinit { + CallSession.shared.removeParticipantEventListener(self) + } + + func onParticipantJoined(participant: Participant) { + // A participant joined the call + } + + func onParticipantLeft(participant: Participant) { + // A participant left the call + } + + func onParticipantListChanged(participants: [Participant]) { + // Participant list updated + } + + func onParticipantAudioMuted(participant: Participant) {} + func onParticipantAudioUnmuted(participant: Participant) {} + func onParticipantVideoPaused(participant: Participant) {} + func onParticipantVideoResumed(participant: Participant) {} + func onParticipantHandRaised(participant: Participant) {} + func onParticipantHandLowered(participant: Participant) {} + func onParticipantStartedScreenShare(participant: Participant) {} + func onParticipantStoppedScreenShare(participant: Participant) {} + func onParticipantStartedRecording(participant: Participant) {} + func onParticipantStoppedRecording(participant: Participant) {} + func onDominantSpeakerChanged(participant: Participant) {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addParticipantEventListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeParticipantEventListener:self]; +} + +- (void)onParticipantJoinedWithParticipant:(Participant *)participant { + // A participant joined the call +} + +- (void)onParticipantLeftWithParticipant:(Participant *)participant { + // A participant left the call +} + +- (void)onParticipantListChangedWithParticipants:(NSArray *)participants { + // Participant list updated +} + +// Other callbacks... + +@end +``` + + + +| Event | Parameter | Description | +|-------|-----------|-------------| +| `onParticipantJoined` | `Participant` | A participant connected to the call | +| `onParticipantLeft` | `Participant` | A participant disconnected from the call | +| `onParticipantListChanged` | `[Participant]` | Participant list updated | +| `onParticipantAudioMuted` | `Participant` | A participant muted their microphone | +| `onParticipantAudioUnmuted` | `Participant` | A participant unmuted their microphone | +| `onParticipantVideoPaused` | `Participant` | A participant turned off their camera | +| `onParticipantVideoResumed` | `Participant` | A participant turned on their camera | +| `onParticipantHandRaised` | `Participant` | A participant raised their hand | +| `onParticipantHandLowered` | `Participant` | A participant lowered their hand | +| `onParticipantStartedScreenShare` | `Participant` | A participant started screen sharing | +| `onParticipantStoppedScreenShare` | `Participant` | A participant stopped screen sharing | +| `onParticipantStartedRecording` | `Participant` | A participant started recording | +| `onParticipantStoppedRecording` | `Participant` | A participant stopped recording | +| `onDominantSpeakerChanged` | `Participant` | The active speaker changed | + +--- + +## Media Events + +Monitor your local media state changes including audio/video status, recording, and device changes. + + + +```swift +class CallViewController: UIViewController, MediaEventsListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addMediaEventsListener(self) + } + + deinit { + CallSession.shared.removeMediaEventsListener(self) + } + + func onAudioMuted() { + // Your microphone was muted + } + + func onAudioUnMuted() { + // Your microphone was unmuted + } + + func onVideoPaused() { + // Your camera was turned off + } + + func onVideoResumed() { + // Your camera was turned on + } + + func onRecordingStarted() { + // Call recording started + } + + func onRecordingStopped() { + // Call recording stopped + } + + func onScreenShareStarted() {} + func onScreenShareStopped() {} + + func onAudioModeChanged(audioModeType: AudioModeType) { + // Audio output device changed + } + + func onCameraFacingChanged(cameraFacing: CameraFacing) { + // Camera switched between front and back + } +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addMediaEventsListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeMediaEventsListener:self]; +} + +- (void)onAudioMuted { + // Your microphone was muted +} + +- (void)onAudioUnMuted { + // Your microphone was unmuted +} + +- (void)onVideoPaused { + // Your camera was turned off +} + +- (void)onVideoResumed { + // Your camera was turned on +} + +- (void)onRecordingStarted { + // Call recording started +} + +- (void)onRecordingStopped { + // Call recording stopped +} + +- (void)onAudioModeChangedWithAudioModeType:(AudioModeType)audioModeType { + // Audio output device changed +} + +- (void)onCameraFacingChangedWithCameraFacing:(CameraFacing)cameraFacing { + // Camera switched between front and back +} + +// Other callbacks... + +@end +``` + + + +| Event | Parameter | Description | +|-------|-----------|-------------| +| `onAudioMuted` | - | Your microphone was muted | +| `onAudioUnMuted` | - | Your microphone was unmuted | +| `onVideoPaused` | - | Your camera was turned off | +| `onVideoResumed` | - | Your camera was turned on | +| `onRecordingStarted` | - | Call recording started | +| `onRecordingStopped` | - | Call recording stopped | +| `onScreenShareStarted` | - | You started screen sharing | +| `onScreenShareStopped` | - | You stopped screen sharing | +| `onAudioModeChanged` | `AudioModeType` | Audio output device changed | +| `onCameraFacingChanged` | `CameraFacing` | Camera switched between front and back | + + + +| Value | Description | +|-------|-------------| +| `.speaker` | Audio routed through device loudspeaker | +| `.earpiece` | Audio routed through phone earpiece | +| `.bluetooth` | Audio routed through connected Bluetooth device | +| `.headphones` | Audio routed through wired headphones | + + + +| Value | Description | +|-------|-------------| +| `.FRONT` | Front-facing (selfie) camera is active | +| `.BACK` | Rear-facing (main) camera is active | + + + +--- + +## Button Click Events + +Intercept UI button clicks from the default call interface to add custom behavior or analytics. + + + +```swift +class CallViewController: UIViewController, ButtonClickListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addButtonClickListener(self) + } + + deinit { + CallSession.shared.removeButtonClickListener(self) + } + + func onLeaveSessionButtonClicked() { + // Leave button tapped + } + + func onToggleAudioButtonClicked() { + // Mute/unmute button tapped + } + + func onToggleVideoButtonClicked() { + // Video on/off button tapped + } + + func onSwitchCameraButtonClicked() {} + func onRaiseHandButtonClicked() {} + func onShareInviteButtonClicked() {} + func onChangeLayoutButtonClicked() {} + func onParticipantListButtonClicked() {} + func onChatButtonClicked() {} + func onRecordingToggleButtonClicked() {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addButtonClickListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeButtonClickListener:self]; +} + +- (void)onLeaveSessionButtonClicked { + // Leave button tapped +} + +- (void)onToggleAudioButtonClicked { + // Mute/unmute button tapped +} + +- (void)onToggleVideoButtonClicked { + // Video on/off button tapped +} + +// Other callbacks... + +@end +``` + + + +| Event | Description | +|-------|-------------| +| `onLeaveSessionButtonClicked` | Leave/end call button was tapped | +| `onToggleAudioButtonClicked` | Mute/unmute button was tapped | +| `onToggleVideoButtonClicked` | Video on/off button was tapped | +| `onSwitchCameraButtonClicked` | Camera switch button was tapped | +| `onRaiseHandButtonClicked` | Raise hand button was tapped | +| `onShareInviteButtonClicked` | Share/invite button was tapped | +| `onChangeLayoutButtonClicked` | Layout change button was tapped | +| `onParticipantListButtonClicked` | Participant list button was tapped | +| `onChatButtonClicked` | In-call chat button was tapped | +| `onRecordingToggleButtonClicked` | Recording toggle button was tapped | + + +Button click events fire before the SDK's default action executes. Use these to add custom logic alongside default behavior. + + +--- + +## Layout Events + +Monitor layout changes including layout type switches and Picture-in-Picture mode transitions. + + + +```swift +class CallViewController: UIViewController, LayoutListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addLayoutListener(self) + } + + deinit { + CallSession.shared.removeLayoutListener(self) + } + + func onCallLayoutChanged(layoutType: LayoutType) { + // Layout changed (TILE, SPOTLIGHT) + } + + func onParticipantListVisible() { + // Participant list panel opened + } + + func onParticipantListHidden() { + // Participant list panel closed + } + + func onPictureInPictureLayoutEnabled() { + // Entered PiP mode + } + + func onPictureInPictureLayoutDisabled() { + // Exited PiP mode + } +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addLayoutListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeLayoutListener:self]; +} + +- (void)onCallLayoutChangedWithLayoutType:(LayoutType)layoutType { + // Layout changed (TILE, SPOTLIGHT) +} + +- (void)onParticipantListVisible { + // Participant list panel opened +} + +- (void)onParticipantListHidden { + // Participant list panel closed +} + +- (void)onPictureInPictureLayoutEnabled { + // Entered PiP mode +} + +- (void)onPictureInPictureLayoutDisabled { + // Exited PiP mode +} + +@end +``` + + + +| Event | Parameter | Description | +|-------|-----------|-------------| +| `onCallLayoutChanged` | `LayoutType` | Call layout changed | +| `onParticipantListVisible` | - | Participant list panel was opened | +| `onParticipantListHidden` | - | Participant list panel was closed | +| `onPictureInPictureLayoutEnabled` | - | Call entered Picture-in-Picture mode | +| `onPictureInPictureLayoutDisabled` | - | Call exited Picture-in-Picture mode | + + +| Value | Description | Best For | +|-------|-------------|----------| +| `.tile` | Grid layout with equally-sized tiles | Group discussions, team meetings | +| `.spotlight` | Large view for active speaker, small tiles for others | Presentations, one-on-one calls | + + +--- + +## Clear All Listeners + +Remove all registered listeners at once. Useful when cleaning up resources. + + + +```swift +CallSession.shared.clearAllListeners() +``` + + +```objectivec +[[CallSession shared] clearAllListeners]; +``` + + diff --git a/calls/ios/idle-timeout.mdx b/calls/ios/idle-timeout.mdx new file mode 100644 index 00000000..8a4fdee4 --- /dev/null +++ b/calls/ios/idle-timeout.mdx @@ -0,0 +1,168 @@ +--- +title: "Idle Timeout" +sidebarTitle: "Idle Timeout" +--- + +Configure automatic session termination when a user is alone in a call. Idle timeout helps manage resources by ending sessions that have no active participants. + +## How Idle Timeout Works + +When a user is the only participant in a call session, the idle timeout countdown begins. If no other participant joins before the timeout expires, the session automatically ends and the `onSessionTimedOut` callback is triggered. + +The timer also restarts when other participants leave and only one user remains in the call. + +```mermaid +flowchart LR + A[Alone in call] --> B[Timer starts] + B --> C{Participant joins?} + C -->|Yes| D[Timer stops] + C -->|No, timeout| E[Session ends] + D -->|Participant leaves| A +``` + +This is useful for: +- Preventing abandoned call sessions from running indefinitely +- Managing server resources efficiently +- Providing a better user experience when the other party doesn't join + +## Configure Idle Timeout + +Set the idle timeout period using `setIdleTimeoutPeriod()` in `SessionSettingsBuilder`. The value is in seconds. + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setIdleTimeoutPeriod(120) // 2 minutes + .setType(.video) + .build() + +CometChatCalls.joinSession( + sessionID: sessionId, + callSetting: sessionSettings, + container: callViewContainer, + onSuccess: { message in + print("Joined session") + }, + onError: { error in + print("Failed: \(error?.errorDescription ?? "")") + } +) +``` + + +```objectivec +SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] + setIdleTimeoutPeriod:120] + setType:CallTypeVideo] + build]; + +[CometChatCalls joinSessionWithSessionID:sessionId + callSetting:sessionSettings + container:self.callViewContainer + onSuccess:^(NSString * message) { + NSLog(@"Joined session"); +} onError:^(CometChatCallException * error) { + NSLog(@"Failed: %@", error.errorDescription); +}]; +``` + + + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `idleTimeoutPeriod` | Int | 300 | Timeout in seconds when alone in the session | + +## Handle Session Timeout + +Listen for the `onSessionTimedOut` callback using `SessionStatusListener` to handle when the session ends due to idle timeout: + + + +```swift +class CallViewController: UIViewController, SessionStatusListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addSessionStatusListener(self) + } + + deinit { + CallSession.shared.removeSessionStatusListener(self) + } + + func onSessionTimedOut() { + print("Session ended due to idle timeout") + // Show message to user + showToast("Call ended - no other participants joined") + // Navigate away from call screen + navigationController?.popViewController(animated: true) + } + + func onSessionJoined() {} + func onSessionLeft() {} + func onConnectionLost() {} + func onConnectionRestored() {} + func onConnectionClosed() {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addSessionStatusListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeSessionStatusListener:self]; +} + +- (void)onSessionTimedOut { + NSLog(@"Session ended due to idle timeout"); + // Show message to user + [self showToast:@"Call ended - no other participants joined"]; + // Navigate away from call screen + [self.navigationController popViewControllerAnimated:YES]; +} + +- (void)onSessionJoined {} +- (void)onSessionLeft {} +- (void)onConnectionLost {} +- (void)onConnectionRestored {} +- (void)onConnectionClosed {} + +@end +``` + + + +## Disable Idle Timeout + +To disable idle timeout and allow sessions to run indefinitely, set a value of `0`: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setIdleTimeoutPeriod(0) // Disable idle timeout + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + setIdleTimeoutPeriod:0] + build]; +``` + + + + +Disabling idle timeout may result in sessions running indefinitely if participants don't join or leave properly. Use with caution. + diff --git a/calls/ios/in-call-chat.mdx b/calls/ios/in-call-chat.mdx new file mode 100644 index 00000000..d484552f --- /dev/null +++ b/calls/ios/in-call-chat.mdx @@ -0,0 +1,530 @@ +--- +title: "In-Call Chat" +sidebarTitle: "In-Call Chat" +--- + +Add real-time messaging to your call experience using CometChat UI Kit. This allows participants to send text messages, share files, and communicate via chat while on a call. + +## Overview + +In-call chat creates a group conversation linked to the call session. When participants tap the chat button, they can: +- Send and receive text messages +- Share images, files, and media +- See message history from the current call +- Get unread message notifications via badge count + +```mermaid +flowchart LR + subgraph "Call Session" + A[Call UI] --> B[Chat Button] + B --> C[Chat VC] + end + + subgraph "CometChat" + D[Group] --> E[Messages] + end + + C <--> D + A -->|Session ID = Group GUID| D +``` + +## Prerequisites + +- CometChat Calls SDK integrated ([Setup](/calls/ios/setup)) +- CometChat Chat SDK integrated ([Chat SDK](/sdk/ios/overview)) +- CometChat UI Kit integrated ([UI Kit](/ui-kit/ios/overview)) + + +The Chat SDK and UI Kit are separate from the Calls SDK. You'll need to add both dependencies to your project. + + +--- + +## Step 1: Add UI Kit Dependency + +Add the CometChat UI Kit to your project via Swift Package Manager or CocoaPods: + +**Swift Package Manager:** +``` +https://github.com/cometchat/cometchat-chat-uikit-ios +``` + +**CocoaPods:** +```ruby +pod 'CometChatUIKitSwift', '~> 4.0' +``` + +--- + +## Step 2: Enable Chat Button + +Configure session settings to show the chat button: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .hideChatButton(false) // Show the chat button + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + hideChatButton:NO] // Show the chat button + build]; +``` + + + +--- + +## Step 3: Create Chat Group + +Create or join a CometChat group using the session ID as the group GUID. This links the chat to the specific call session. + + + +```swift +private func setupChatGroup(sessionId: String, meetingName: String) { + // Try to get existing group first + CometChat.getGroup(GUID: sessionId) { group in + if !group.hasJoined { + self.joinGroup(guid: sessionId, groupType: group.groupType) + } else { + print("Already joined group: \(group.name ?? "")") + } + } onError: { error in + if error?.errorCode == "ERR_GUID_NOT_FOUND" { + // Group doesn't exist, create it + self.createGroup(guid: sessionId, name: meetingName) + } else { + print("Error getting group: \(error?.errorDescription ?? "")") + } + } +} + +private func createGroup(guid: String, name: String) { + let group = Group(guid: guid, name: name, groupType: .public, password: nil) + + CometChat.createGroup(group: group) { createdGroup in + print("Group created: \(createdGroup.name ?? "")") + } onError: { error in + print("Group creation failed: \(error?.errorDescription ?? "")") + } +} + +private func joinGroup(guid: String, groupType: CometChat.groupType) { + CometChat.joinGroup(GUID: guid, groupType: groupType, password: nil) { joinedGroup in + print("Joined group: \(joinedGroup.name ?? "")") + } onError: { error in + print("Join group failed: \(error?.errorDescription ?? "")") + } +} +``` + + +```objectivec +- (void)setupChatGroupWithSessionId:(NSString *)sessionId meetingName:(NSString *)meetingName { + [CometChat getGroupWithGUID:sessionId onSuccess:^(Group * group) { + if (!group.hasJoined) { + [self joinGroupWithGuid:sessionId groupType:group.groupType]; + } else { + NSLog(@"Already joined group: %@", group.name); + } + } onError:^(CometChatException * error) { + if ([error.errorCode isEqualToString:@"ERR_GUID_NOT_FOUND"]) { + [self createGroupWithGuid:sessionId name:meetingName]; + } else { + NSLog(@"Error getting group: %@", error.errorDescription); + } + }]; +} + +- (void)createGroupWithGuid:(NSString *)guid name:(NSString *)name { + Group *group = [[Group alloc] initWithGuid:guid name:name groupType:GroupTypePublic password:nil]; + + [CometChat createGroupWithGroup:group onSuccess:^(Group * createdGroup) { + NSLog(@"Group created: %@", createdGroup.name); + } onError:^(CometChatException * error) { + NSLog(@"Group creation failed: %@", error.errorDescription); + }]; +} + +- (void)joinGroupWithGuid:(NSString *)guid groupType:(GroupType)groupType { + [CometChat joinGroupWithGUID:guid groupType:groupType password:nil onSuccess:^(Group * joinedGroup) { + NSLog(@"Joined group: %@", joinedGroup.name); + } onError:^(CometChatException * error) { + NSLog(@"Join group failed: %@", error.errorDescription); + }]; +} +``` + + + +--- + +## Step 4: Handle Chat Button Click + +Listen for the chat button click and open your chat view controller: + + + +```swift +private var unreadMessageCount = 0 + +private func setupChatButtonListener() { + CallSession.shared.addButtonClickListener(self) +} + +extension CallViewController: ButtonClickListener { + + func onChatButtonClicked() { + // Reset unread count when opening chat + unreadMessageCount = 0 + CallSession.shared.setChatButtonUnreadCount(0) + + // Open chat view controller + let chatVC = ChatViewController() + chatVC.sessionId = sessionId + chatVC.meetingName = meetingName + + let navController = UINavigationController(rootViewController: chatVC) + navController.modalPresentationStyle = .pageSheet + + if let sheet = navController.sheetPresentationController { + sheet.detents = [.medium(), .large()] + sheet.prefersGrabberVisible = true + } + + present(navController, animated: true) + } +} +``` + + +```objectivec +@interface CallViewController () +@property (nonatomic, assign) NSInteger unreadMessageCount; +@end + +- (void)setupChatButtonListener { + [[CallSession shared] addButtonClickListener:self]; +} + +- (void)onChatButtonClicked { + // Reset unread count when opening chat + self.unreadMessageCount = 0; + [[CallSession shared] setChatButtonUnreadCount:0]; + + // Open chat view controller + ChatViewController *chatVC = [[ChatViewController alloc] init]; + chatVC.sessionId = self.sessionId; + chatVC.meetingName = self.meetingName; + + UINavigationController *navController = [[UINavigationController alloc] initWithRootViewController:chatVC]; + navController.modalPresentationStyle = UIModalPresentationPageSheet; + + UISheetPresentationController *sheet = navController.sheetPresentationController; + if (sheet) { + sheet.detents = @[UISheetPresentationControllerDetent.mediumDetent, UISheetPresentationControllerDetent.largeDetent]; + sheet.prefersGrabberVisible = YES; + } + + [self presentViewController:navController animated:YES completion:nil]; +} +``` + + + +--- + +## Step 5: Track Unread Messages + +Listen for incoming messages and update the badge count on the chat button: + + + +```swift +private func setupMessageListener() { + CometChat.addMessageListener("CallChatListener", self) +} + +extension CallViewController: CometChatMessageDelegate { + + func onTextMessageReceived(textMessage: TextMessage) { + // Check if message is for our call's group + if let receiver = textMessage.receiver as? Group, + receiver.guid == sessionId { + unreadMessageCount += 1 + CallSession.shared.setChatButtonUnreadCount(unreadMessageCount) + } + } + + func onMediaMessageReceived(mediaMessage: MediaMessage) { + if let receiver = mediaMessage.receiver as? Group, + receiver.guid == sessionId { + unreadMessageCount += 1 + CallSession.shared.setChatButtonUnreadCount(unreadMessageCount) + } + } +} + +deinit { + CometChat.removeMessageListener("CallChatListener") +} +``` + + +```objectivec +- (void)setupMessageListener { + [CometChat addMessageListener:@"CallChatListener" delegate:self]; +} + +- (void)onTextMessageReceived:(TextMessage *)textMessage { + if ([textMessage.receiver isKindOfClass:[Group class]]) { + Group *group = (Group *)textMessage.receiver; + if ([group.guid isEqualToString:self.sessionId]) { + self.unreadMessageCount++; + [[CallSession shared] setChatButtonUnreadCount:self.unreadMessageCount]; + } + } +} + +- (void)onMediaMessageReceived:(MediaMessage *)mediaMessage { + if ([mediaMessage.receiver isKindOfClass:[Group class]]) { + Group *group = (Group *)mediaMessage.receiver; + if ([group.guid isEqualToString:self.sessionId]) { + self.unreadMessageCount++; + [[CallSession shared] setChatButtonUnreadCount:self.unreadMessageCount]; + } + } +} + +- (void)dealloc { + [CometChat removeMessageListener:@"CallChatListener"]; +} +``` + + + +--- + +## Step 6: Create Chat View Controller + +Create a chat view controller using UI Kit components: + + + +```swift +import CometChatUIKitSwift + +class ChatViewController: UIViewController { + + var sessionId: String = "" + var meetingName: String = "" + + private let messageList = CometChatMessageList() + private let messageComposer = CometChatMessageComposer() + private let activityIndicator = UIActivityIndicatorView(style: .large) + + override func viewDidLoad() { + super.viewDidLoad() + setupUI() + loadGroup() + } + + private func setupUI() { + view.backgroundColor = .systemBackground + title = meetingName + + navigationItem.rightBarButtonItem = UIBarButtonItem( + barButtonSystemItem: .close, + target: self, + action: #selector(dismissView) + ) + + // Setup message list + messageList.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(messageList) + + // Setup message composer + messageComposer.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(messageComposer) + + // Setup activity indicator + activityIndicator.translatesAutoresizingMaskIntoConstraints = false + activityIndicator.hidesWhenStopped = true + view.addSubview(activityIndicator) + + NSLayoutConstraint.activate([ + messageList.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), + messageList.leadingAnchor.constraint(equalTo: view.leadingAnchor), + messageList.trailingAnchor.constraint(equalTo: view.trailingAnchor), + messageList.bottomAnchor.constraint(equalTo: messageComposer.topAnchor), + + messageComposer.leadingAnchor.constraint(equalTo: view.leadingAnchor), + messageComposer.trailingAnchor.constraint(equalTo: view.trailingAnchor), + messageComposer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor), + + activityIndicator.centerXAnchor.constraint(equalTo: view.centerXAnchor), + activityIndicator.centerYAnchor.constraint(equalTo: view.centerYAnchor) + ]) + } + + @objc private func dismissView() { + dismiss(animated: true) + } + + private func loadGroup() { + activityIndicator.startAnimating() + + CometChat.getGroup(GUID: sessionId) { [weak self] group in + guard let self = self else { return } + + if !group.hasJoined { + self.joinAndSetGroup(guid: self.sessionId, groupType: group.groupType) + } else { + self.setGroup(group) + } + } onError: { [weak self] error in + guard let self = self else { return } + + if error?.errorCode == "ERR_GUID_NOT_FOUND" { + self.createAndSetGroup() + } else { + self.activityIndicator.stopAnimating() + print("Error: \(error?.errorDescription ?? "")") + } + } + } + + private func createAndSetGroup() { + let group = Group(guid: sessionId, name: meetingName, groupType: .public, password: nil) + + CometChat.createGroup(group: group) { [weak self] createdGroup in + self?.setGroup(createdGroup) + } onError: { [weak self] error in + self?.activityIndicator.stopAnimating() + } + } + + private func joinAndSetGroup(guid: String, groupType: CometChat.groupType) { + CometChat.joinGroup(GUID: guid, groupType: groupType, password: nil) { [weak self] joinedGroup in + self?.setGroup(joinedGroup) + } onError: { [weak self] error in + self?.activityIndicator.stopAnimating() + } + } + + private func setGroup(_ group: Group) { + activityIndicator.stopAnimating() + + messageList.set(group: group) + messageComposer.set(group: group) + } +} +``` + + +```objectivec +#import + +@interface ChatViewController () +@property (nonatomic, strong) CometChatMessageList *messageList; +@property (nonatomic, strong) CometChatMessageComposer *messageComposer; +@property (nonatomic, strong) UIActivityIndicatorView *activityIndicator; +@property (nonatomic, copy) NSString *sessionId; +@property (nonatomic, copy) NSString *meetingName; +@end + +@implementation ChatViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [self setupUI]; + [self loadGroup]; +} + +- (void)setupUI { + self.view.backgroundColor = [UIColor systemBackgroundColor]; + self.title = self.meetingName; + + self.navigationItem.rightBarButtonItem = [[UIBarButtonItem alloc] + initWithBarButtonSystemItem:UIBarButtonSystemItemClose + target:self + action:@selector(dismissView)]; + + // Setup message list + self.messageList = [[CometChatMessageList alloc] init]; + self.messageList.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.messageList]; + + // Setup message composer + self.messageComposer = [[CometChatMessageComposer alloc] init]; + self.messageComposer.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.messageComposer]; + + // Setup activity indicator + self.activityIndicator = [[UIActivityIndicatorView alloc] initWithActivityIndicatorStyle:UIActivityIndicatorViewStyleLarge]; + self.activityIndicator.translatesAutoresizingMaskIntoConstraints = NO; + self.activityIndicator.hidesWhenStopped = YES; + [self.view addSubview:self.activityIndicator]; + + [NSLayoutConstraint activateConstraints:@[ + [self.messageList.topAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.topAnchor], + [self.messageList.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [self.messageList.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [self.messageList.bottomAnchor constraintEqualToAnchor:self.messageComposer.topAnchor], + + [self.messageComposer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [self.messageComposer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [self.messageComposer.bottomAnchor constraintEqualToAnchor:self.view.safeAreaLayoutGuide.bottomAnchor], + + [self.activityIndicator.centerXAnchor constraintEqualToAnchor:self.view.centerXAnchor], + [self.activityIndicator.centerYAnchor constraintEqualToAnchor:self.view.centerYAnchor] + ]]; +} + +- (void)dismissView { + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)loadGroup { + [self.activityIndicator startAnimating]; + + __weak typeof(self) weakSelf = self; + [CometChat getGroupWithGUID:self.sessionId onSuccess:^(Group * group) { + if (!group.hasJoined) { + [weakSelf joinAndSetGroupWithGuid:weakSelf.sessionId groupType:group.groupType]; + } else { + [weakSelf setGroup:group]; + } + } onError:^(CometChatException * error) { + if ([error.errorCode isEqualToString:@"ERR_GUID_NOT_FOUND"]) { + [weakSelf createAndSetGroup]; + } else { + [weakSelf.activityIndicator stopAnimating]; + } + }]; +} + +- (void)setGroup:(Group *)group { + [self.activityIndicator stopAnimating]; + + [self.messageList setWithGroup:group]; + [self.messageComposer setWithGroup:group]; +} + +@end +``` + + + +--- + +## Related Documentation + +- [UI Kit Overview](/ui-kit/ios/overview) - CometChat UI Kit components +- [Events](/calls/ios/events) - Button click events +- [Session Settings](/calls/ios/session-settings) - Configure chat button visibility diff --git a/calls/ios/join-session.mdx b/calls/ios/join-session.mdx new file mode 100644 index 00000000..c3c01e73 --- /dev/null +++ b/calls/ios/join-session.mdx @@ -0,0 +1,278 @@ +--- +title: "Join Session" +sidebarTitle: "Join Session" +--- + +Join a call session using one of two approaches: the quick start method with a session ID, or the advanced flow with manual token generation for more control. + +## Overview + +The CometChat Calls SDK provides two ways to join a session: + +| Approach | Best For | Complexity | +|----------|----------|------------| +| **Join with Session ID** | Most use cases - simple and straightforward | Low - One method call | +| **Join with Token** | Custom token management, pre-generation, caching | Medium - Two-step process | + + +Both approaches require a container view in your layout and properly configured [SessionSettings](/calls/ios/session-settings). + + +## Container Setup + +Create a container view where the call interface will be rendered. This can be done in your storyboard or programmatically: + + + +```swift +// Programmatically +let callViewContainer = UIView() +callViewContainer.translatesAutoresizingMaskIntoConstraints = false +view.addSubview(callViewContainer) + +NSLayoutConstraint.activate([ + callViewContainer.topAnchor.constraint(equalTo: view.topAnchor), + callViewContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor), + callViewContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), + callViewContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor) +]) +``` + + +Add a `UIView` to your view controller and create an `@IBOutlet`: + +```swift +@IBOutlet weak var callViewContainer: UIView! +``` + + + +The call UI will be dynamically added to this container when you join the session. + +## Join with Session ID + +The simplest way to join a session. Pass a session ID and the SDK automatically generates the token and joins the call. + + + +```swift +let sessionId = "SESSION_ID" + +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setDisplayName("John Doe") + .setType(.video) + .build() + +CometChatCalls.joinSession( + sessionID: sessionId, + callSetting: sessionSettings, + container: callViewContainer, + onSuccess: { message in + print("Joined session successfully") + }, + onError: { error in + print("Failed: \(error?.errorDescription ?? "")") + } +) +``` + + +```objectivec +NSString *sessionId = @"SESSION_ID"; + +SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] + setDisplayName:@"John Doe"] + setType:CallTypeVideo] + build]; + +[CometChatCalls joinSessionWithSessionID:sessionId + callSetting:sessionSettings + container:self.callViewContainer + onSuccess:^(NSString * message) { + NSLog(@"Joined session successfully"); +} onError:^(CometChatCallException * error) { + NSLog(@"Failed: %@", error.errorDescription); +}]; +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sessionID` | String | Unique identifier for the call session | +| `callSetting` | SessionSettings | Configuration for the session | +| `container` | UIView | Container view for the call UI | +| `onSuccess` | Closure | Closure called on successful join | +| `onError` | Closure | Closure called on failure | + + +All participants joining the same call must use the same session ID. + + +## Join with Token + +For scenarios requiring more control over token generation, such as pre-generating tokens, implementing custom caching strategies, or managing token lifecycle separately. + +**Step 1: Generate Token** + +Generate a call token for the session. Each token is unique to a specific session and user combination. + + + +```swift +let sessionId = "SESSION_ID" + +CometChatCalls.generateToken(sessionID: sessionId, onSuccess: { token in + print("Token generated: \(token ?? "")") + // Store or use the token +}, onError: { error in + print("Token generation failed: \(error?.errorDescription ?? "")") +}) +``` + + +```objectivec +NSString *sessionId = @"SESSION_ID"; + +[CometChatCalls generateTokenWithSessionID:sessionId + onSuccess:^(NSString * token) { + NSLog(@"Token generated: %@", token); + // Store or use the token +} onError:^(CometChatCallException * error) { + NSLog(@"Token generation failed: %@", error.errorDescription); +}]; +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sessionID` | String | Unique identifier for the call session | +| `onSuccess` | Closure | Closure returning the generated token string | +| `onError` | Closure | Closure called on failure | + +**Step 2: Join with Token** + +Use the generated token to join the session. This gives you control over when and how the token is used. + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setDisplayName("John Doe") + .setType(.video) + .build() + +// Use the previously generated token +CometChatCalls.joinSession( + callToken: generatedToken, + callSetting: sessionSettings, + container: callViewContainer, + onSuccess: { message in + print("Joined session successfully") + }, + onError: { error in + print("Failed: \(error?.errorDescription ?? "")") + } +) +``` + + +```objectivec +SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] + setDisplayName:@"John Doe"] + setType:CallTypeVideo] + build]; + +// Use the previously generated token +[CometChatCalls joinSessionWithCallToken:generatedToken + callSetting:sessionSettings + container:self.callViewContainer + onSuccess:^(NSString * message) { + NSLog(@"Joined session successfully"); +} onError:^(CometChatCallException * error) { + NSLog(@"Failed: %@", error.errorDescription); +}]; +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `callToken` | String | Previously generated token string | +| `callSetting` | SessionSettings | Configuration for the session | +| `container` | UIView | Container view for the call UI | +| `onSuccess` | Closure | Closure called on successful join | +| `onError` | Closure | Closure called on failure | + +**Complete Example** + + + +```swift +let sessionId = "SESSION_ID" + +// Step 1: Generate token +CometChatCalls.generateToken(sessionID: sessionId, onSuccess: { [weak self] token in + guard let self = self, let token = token else { return } + + // Step 2: Join with token + let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setDisplayName("John Doe") + .setType(.video) + .build() + + CometChatCalls.joinSession( + callToken: token, + callSetting: sessionSettings, + container: self.callViewContainer, + onSuccess: { message in + print("Joined session successfully") + }, + onError: { error in + print("Failed to join: \(error?.errorDescription ?? "")") + } + ) +}, onError: { error in + print("Token generation failed: \(error?.errorDescription ?? "")") +}) +``` + + +```objectivec +NSString *sessionId = @"SESSION_ID"; + +// Step 1: Generate token +[CometChatCalls generateTokenWithSessionID:sessionId + onSuccess:^(NSString * token) { + // Step 2: Join with token + SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] + setDisplayName:@"John Doe"] + setType:CallTypeVideo] + build]; + + [CometChatCalls joinSessionWithCallToken:token + callSetting:sessionSettings + container:self.callViewContainer + onSuccess:^(NSString * message) { + NSLog(@"Joined session successfully"); + } onError:^(CometChatCallException * error) { + NSLog(@"Failed to join: %@", error.errorDescription); + }]; +} onError:^(CometChatCallException * error) { + NSLog(@"Token generation failed: %@", error.errorDescription); +}]; +``` + + + +## Error Handling + +Common errors when joining a session: + +| Error Code | Description | +|------------|-------------| +| `INIT ERROR` | SDK not initialized - call `init()` first | +| `AUTH TOKEN ERROR` | User not logged in or auth token invalid | +| `SESSION ID ERROR` | Session ID is null or empty | +| `API ERROR` | Invalid call token or API error | diff --git a/calls/ios/overview.mdx b/calls/ios/overview.mdx index 3d0f2822..4195ffa0 100644 --- a/calls/ios/overview.mdx +++ b/calls/ios/overview.mdx @@ -3,4 +3,105 @@ title: "Calls SDK" sidebarTitle: "Overview" --- -Documentation coming soon for iOS Calls SDK. +The CometChat Calls SDK enables real-time voice and video calling capabilities in your iOS application. Built on top of WebRTC, it provides a complete calling solution with built-in UI components and extensive customization options. + + +**Faster Integration with UI Kits** + +If you're using CometChat UI Kits, voice and video calling is already integrated with ready-to-use components: +- Incoming & outgoing call screens +- Call buttons with one-tap calling +- Call logs with history + +👉 [iOS UI Kit Call Features](/ui-kit/ios/call-features) + +Use this Calls SDK directly only if you need custom call UI or advanced control. + + +## Prerequisites + +Before integrating the Calls SDK, ensure you have: + +1. **CometChat Account**: [Sign up](https://app.cometchat.com/signup) and create an app to get your App ID, Region, and API Key +2. **CometChat Users**: Users must exist in CometChat to use calling features. For testing, create users via the [Dashboard](https://app.cometchat.com) or [REST API](/rest-api/chat-apis/users/create-user). Authentication is handled by the Calls SDK - see [Authentication](/calls/ios/authentication) +3. **iOS Requirements**: + - Minimum iOS version: 13.0 + - Xcode 14.0 or later + - Swift 5.0 or later +4. **Permissions**: Camera and microphone permissions for video/audio calls + +## Call Flow + +```mermaid +sequenceDiagram + participant App + participant CometChatCalls + participant CallSession + + App->>CometChatCalls: init() + App->>CometChatCalls: login() + App->>CometChatCalls: generateToken() + App->>CometChatCalls: joinSession() + CometChatCalls-->>App: CallSession.shared + App->>CallSession: Actions (mute, pause, etc.) + CallSession-->>App: Event callbacks + App->>CallSession: leaveSession() +``` + +## Features + + + + + Incoming and outgoing call notifications with accept/reject functionality + + + + Tile and Spotlight view modes for different call scenarios + + + + Switch between speaker, earpiece, Bluetooth, and headphones + + + + Record call sessions for later playback + + + + Retrieve call history and details + + + + Mute, pin, and manage call participants + + + + View screen shares from other participants + + + + Continue calls while using other apps + + + + Signal to get attention during calls + + + + Automatic session termination when alone in a call + + + + +## Architecture + +The SDK is organized around these core components: + +| Component | Description | +|-----------|-------------| +| `CometChatCalls` | Main entry point for SDK initialization, authentication, and session management | +| `CallAppSettings` | Configuration for SDK initialization (App ID, Region) | +| `SessionSettings` | Configuration for individual call sessions | +| `CallSession` | Singleton that manages the active call and provides control methods | +| `Listeners` | Protocol-based event interfaces for session, participant, media, and UI events | diff --git a/calls/ios/participant-management.mdx b/calls/ios/participant-management.mdx new file mode 100644 index 00000000..74c5e8bd --- /dev/null +++ b/calls/ios/participant-management.mdx @@ -0,0 +1,229 @@ +--- +title: "Participant Management" +sidebarTitle: "Participant Management" +--- + +Manage participants during a call with actions like muting, pausing video, and pinning. These features help maintain order in group calls and highlight important speakers. + + +By default, all participants who join a call have moderator access and can perform these actions. Implementing role-based moderation (e.g., restricting actions to hosts only) is the responsibility of the app developer based on their use case. + + +## Mute a Participant + +Mute a specific participant's audio. This affects the participant for all users in the call. + + + +```swift +CallSession.shared.muteParticipant(participantId: participant.pid) +``` + + +```objectivec +[[CallSession shared] muteParticipantWithParticipantId:participant.pid]; +``` + + + +## Pause Participant Video + +Pause a specific participant's video. This affects the participant for all users in the call. + + + +```swift +CallSession.shared.pauseParticipantVideo(participantId: participant.pid) +``` + + +```objectivec +[[CallSession shared] pauseParticipantVideoWithParticipantId:participant.pid]; +``` + + + +## Pin a Participant + +Pin a participant to keep them prominently displayed regardless of who is speaking. Useful for keeping focus on a presenter or important speaker. + + + +```swift +// Pin a participant +CallSession.shared.pinParticipant() + +// Unpin (returns to automatic speaker highlighting) +CallSession.shared.unPinParticipant() +``` + + +```objectivec +// Pin a participant +[[CallSession shared] pinParticipant]; + +// Unpin (returns to automatic speaker highlighting) +[[CallSession shared] unPinParticipant]; +``` + + + + +Pinning a participant only affects your local view. Other participants can pin different users independently. + + +## Listen for Participant Events + +Monitor participant state changes using `ParticipantEventListener`: + + + +```swift +class CallViewController: UIViewController, ParticipantEventListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addParticipantEventListener(self) + } + + deinit { + CallSession.shared.removeParticipantEventListener(self) + } + + func onParticipantJoined(participant: Participant) { + print("\(participant.name ?? "") joined the call") + updateParticipantList() + } + + func onParticipantLeft(participant: Participant) { + print("\(participant.name ?? "") left the call") + updateParticipantList() + } + + func onParticipantListChanged(participants: [Participant]) { + print("Participant count: \(participants.count)") + refreshParticipantList(participants) + } + + func onParticipantAudioMuted(participant: Participant) { + print("\(participant.name ?? "") was muted") + updateMuteIndicator(participant, muted: true) + } + + func onParticipantAudioUnmuted(participant: Participant) { + print("\(participant.name ?? "") was unmuted") + updateMuteIndicator(participant, muted: false) + } + + func onParticipantVideoPaused(participant: Participant) { + print("\(participant.name ?? "") video paused") + showParticipantAvatar(participant) + } + + func onParticipantVideoResumed(participant: Participant) { + print("\(participant.name ?? "") video resumed") + showParticipantVideo(participant) + } + + func onDominantSpeakerChanged(participant: Participant) { + print("\(participant.name ?? "") is now speaking") + highlightActiveSpeaker(participant) + } + + // Other callbacks... + func onParticipantHandRaised(participant: Participant) {} + func onParticipantHandLowered(participant: Participant) {} + func onParticipantStartedScreenShare(participant: Participant) {} + func onParticipantStoppedScreenShare(participant: Participant) {} + func onParticipantStartedRecording(participant: Participant) {} + func onParticipantStoppedRecording(participant: Participant) {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addParticipantEventListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeParticipantEventListener:self]; +} + +- (void)onParticipantJoinedWithParticipant:(Participant *)participant { + NSLog(@"%@ joined the call", participant.name); + [self updateParticipantList]; +} + +- (void)onParticipantLeftWithParticipant:(Participant *)participant { + NSLog(@"%@ left the call", participant.name); + [self updateParticipantList]; +} + +- (void)onParticipantListChangedWithParticipants:(NSArray *)participants { + NSLog(@"Participant count: %lu", (unsigned long)participants.count); + [self refreshParticipantList:participants]; +} + +- (void)onParticipantAudioMutedWithParticipant:(Participant *)participant { + NSLog(@"%@ was muted", participant.name); +} + +- (void)onParticipantAudioUnmutedWithParticipant:(Participant *)participant { + NSLog(@"%@ was unmuted", participant.name); +} + +- (void)onDominantSpeakerChangedWithParticipant:(Participant *)participant { + NSLog(@"%@ is now speaking", participant.name); +} + +// Other callbacks... + +@end +``` + + + +## Participant Object + +The `Participant` object contains information about each call participant: + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | String | Unique identifier (CometChat user ID) | +| `name` | String | Display name | +| `avatar` | String | URL of avatar image | +| `pid` | String | Participant ID for this call session | +| `role` | String | Role in the call | +| `audioMuted` | Bool | Whether audio is muted | +| `videoPaused` | Bool | Whether video is paused | +| `isPinned` | Bool | Whether pinned in layout | +| `isPresenting` | Bool | Whether screen sharing | +| `raisedHandTimestamp` | Int | Timestamp when hand was raised (0 if not raised) | + +## Hide Participant List Button + +To hide the participant list button in the call UI: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .hideParticipantListButton(true) + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + hideParticipantListButton:YES] + build]; +``` + + diff --git a/calls/ios/picture-in-picture.mdx b/calls/ios/picture-in-picture.mdx new file mode 100644 index 00000000..abc68386 --- /dev/null +++ b/calls/ios/picture-in-picture.mdx @@ -0,0 +1,174 @@ +--- +title: "Picture-in-Picture" +sidebarTitle: "Picture-in-Picture" +--- + +Enable Picture-in-Picture (PiP) mode to allow users to continue their call in a floating window while using other apps. PiP provides a seamless multitasking experience during calls. + + +Picture-in-Picture implementation is handled at the app level using iOS's PiP APIs. The Calls SDK only adjusts the call UI layout to fit the PiP window - it does not manage the PiP window itself. + + +## How It Works + +1. Your app enters PiP mode using iOS's AVPictureInPictureController API +2. You notify the Calls SDK by calling `enablePictureInPictureLayout()` +3. The SDK adjusts the call UI to fit the smaller PiP window (hides controls, optimizes layout) +4. When exiting PiP, call `disablePictureInPictureLayout()` to restore the full UI + +## Enable Picture-in-Picture + +Enter PiP mode programmatically using the `enablePictureInPictureLayout()` action: + + + +```swift +CallSession.shared.enablePictureInPictureLayout() +``` + + +```objectivec +[[CallSession shared] enablePictureInPictureLayout]; +``` + + + +## Disable Picture-in-Picture + +Exit PiP mode and return to the full-screen call interface: + + + +```swift +CallSession.shared.disablePictureInPictureLayout() +``` + + +```objectivec +[[CallSession shared] disablePictureInPictureLayout]; +``` + + + +## Listen for PiP Events + +Monitor PiP mode transitions using `LayoutListener` to update your UI accordingly: + + + +```swift +class CallViewController: UIViewController, LayoutListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addLayoutListener(self) + } + + deinit { + CallSession.shared.removeLayoutListener(self) + } + + func onPictureInPictureLayoutEnabled() { + print("Entered PiP mode") + // Hide custom overlays or controls + hideCustomControls() + } + + func onPictureInPictureLayoutDisabled() { + print("Exited PiP mode") + // Show custom overlays or controls + showCustomControls() + } + + func onCallLayoutChanged(layoutType: LayoutType) {} + func onParticipantListVisible() {} + func onParticipantListHidden() {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addLayoutListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeLayoutListener:self]; +} + +- (void)onPictureInPictureLayoutEnabled { + NSLog(@"Entered PiP mode"); + // Hide custom overlays or controls + [self hideCustomControls]; +} + +- (void)onPictureInPictureLayoutDisabled { + NSLog(@"Exited PiP mode"); + // Show custom overlays or controls + [self showCustomControls]; +} + +- (void)onCallLayoutChangedWithLayoutType:(LayoutType)layoutType {} +- (void)onParticipantListVisible {} +- (void)onParticipantListHidden {} + +@end +``` + + + +## iOS PiP Setup + +To enable PiP in your iOS app: + +**1. Enable Background Modes** + +In your project's **Signing & Capabilities**, add the **Background Modes** capability and enable: +- Audio, AirPlay, and Picture in Picture + +**2. Configure AVAudioSession** + + + +```swift +import AVFoundation + +func configureAudioSession() { + do { + try AVAudioSession.sharedInstance().setCategory( + .playAndRecord, + mode: .videoChat, + options: [.allowBluetooth, .defaultToSpeaker] + ) + try AVAudioSession.sharedInstance().setActive(true) + } catch { + print("Failed to configure audio session: \(error)") + } +} +``` + + +```objectivec +#import + +- (void)configureAudioSession { + NSError *error = nil; + [[AVAudioSession sharedInstance] setCategory:AVAudioSessionCategoryPlayAndRecord + mode:AVAudioSessionModeVideoChat + options:AVAudioSessionCategoryOptionAllowBluetooth | AVAudioSessionCategoryOptionDefaultToSpeaker + error:&error]; + [[AVAudioSession sharedInstance] setActive:YES error:&error]; +} +``` + + + + +PiP mode is available on iOS 14.0 and later for iPhones, and iOS 9.0 and later for iPads. + diff --git a/calls/ios/raise-hand.mdx b/calls/ios/raise-hand.mdx new file mode 100644 index 00000000..b7c74688 --- /dev/null +++ b/calls/ios/raise-hand.mdx @@ -0,0 +1,178 @@ +--- +title: "Raise Hand" +sidebarTitle: "Raise Hand" +--- + +Allow participants to raise their hand to get attention during calls. This feature is useful for large meetings, webinars, or any scenario where participants need to signal they want to speak. + +## Raise Hand + +Signal that you want to speak or get attention: + + + +```swift +CallSession.shared.raiseHand() +``` + + +```objectivec +[[CallSession shared] raiseHand]; +``` + + + +## Lower Hand + +Remove the raised hand indicator: + + + +```swift +CallSession.shared.lowerHand() +``` + + +```objectivec +[[CallSession shared] lowerHand]; +``` + + + +## Listen for Raise Hand Events + +Monitor when participants raise or lower their hands using `ParticipantEventListener`: + + + +```swift +class CallViewController: UIViewController, ParticipantEventListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addParticipantEventListener(self) + } + + deinit { + CallSession.shared.removeParticipantEventListener(self) + } + + func onParticipantHandRaised(participant: Participant) { + print("\(participant.name ?? "") raised their hand") + // Show notification or visual indicator + showHandRaisedNotification(participant) + } + + func onParticipantHandLowered(participant: Participant) { + print("\(participant.name ?? "") lowered their hand") + // Remove notification or visual indicator + hideHandRaisedIndicator(participant) + } + + // Other callbacks... + func onParticipantJoined(participant: Participant) {} + func onParticipantLeft(participant: Participant) {} + func onParticipantListChanged(participants: [Participant]) {} + func onParticipantAudioMuted(participant: Participant) {} + func onParticipantAudioUnmuted(participant: Participant) {} + func onParticipantVideoPaused(participant: Participant) {} + func onParticipantVideoResumed(participant: Participant) {} + func onParticipantStartedScreenShare(participant: Participant) {} + func onParticipantStoppedScreenShare(participant: Participant) {} + func onParticipantStartedRecording(participant: Participant) {} + func onParticipantStoppedRecording(participant: Participant) {} + func onDominantSpeakerChanged(participant: Participant) {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addParticipantEventListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeParticipantEventListener:self]; +} + +- (void)onParticipantHandRaisedWithParticipant:(Participant *)participant { + NSLog(@"%@ raised their hand", participant.name); + // Show notification or visual indicator + [self showHandRaisedNotification:participant]; +} + +- (void)onParticipantHandLoweredWithParticipant:(Participant *)participant { + NSLog(@"%@ lowered their hand", participant.name); + // Remove notification or visual indicator + [self hideHandRaisedIndicator:participant]; +} + +// Other callbacks... + +@end +``` + + + +## Check Raised Hand Status + +The `Participant` object includes a `raisedHandTimestamp` property to check if a participant has their hand raised: + + + +```swift +func onParticipantListChanged(participants: [Participant]) { + let raisedHands = participants + .filter { $0.raisedHandTimestamp > 0 } + .sorted { $0.raisedHandTimestamp < $1.raisedHandTimestamp } + + // Display participants with raised hands in order + updateRaisedHandsList(raisedHands) +} +``` + + +```objectivec +- (void)onParticipantListChangedWithParticipants:(NSArray *)participants { + NSMutableArray *raisedHands = [NSMutableArray array]; + for (Participant *p in participants) { + if (p.raisedHandTimestamp > 0) { + [raisedHands addObject:p]; + } + } + // Sort by timestamp and display + [raisedHands sortUsingComparator:^NSComparisonResult(Participant *a, Participant *b) { + return [@(a.raisedHandTimestamp) compare:@(b.raisedHandTimestamp)]; + }]; + [self updateRaisedHandsList:raisedHands]; +} +``` + + + +## Hide Raise Hand Button + +To disable the raise hand feature, hide the button in the call UI: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .hideRaiseHandButton(true) + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + hideRaiseHandButton:YES] + build]; +``` + + diff --git a/calls/ios/recording.mdx b/calls/ios/recording.mdx new file mode 100644 index 00000000..70909fe1 --- /dev/null +++ b/calls/ios/recording.mdx @@ -0,0 +1,218 @@ +--- +title: "Recording" +sidebarTitle: "Recording" +--- + +Record call sessions for later playback. Recordings are stored server-side and can be accessed through call logs or the CometChat Dashboard. + + +Recording must be enabled for your CometChat app. Contact support or check your Dashboard settings if recording is not available. + + +## Start Recording + +Start recording during an active call session: + + + +```swift +CallSession.shared.startRecording() +``` + + +```objectivec +[[CallSession shared] startRecording]; +``` + + + +All participants are notified when recording starts. + +## Stop Recording + +Stop an active recording: + + + +```swift +CallSession.shared.stopRecording() +``` + + +```objectivec +[[CallSession shared] stopRecording]; +``` + + + +## Auto-Start Recording + +Configure calls to automatically start recording when the session begins: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .enableAutoStartRecording(true) + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + enableAutoStartRecording:YES] + build]; +``` + + + +## Hide Recording Button + +Hide the recording button from the default call UI: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .hideRecordingButton(true) + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + hideRecordingButton:YES] + build]; +``` + + + +## Listen for Recording Events + +Monitor recording state changes using `MediaEventsListener`: + + + +```swift +class CallViewController: UIViewController, MediaEventsListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addMediaEventsListener(self) + } + + deinit { + CallSession.shared.removeMediaEventsListener(self) + } + + func onRecordingStarted() { + print("Recording started") + showRecordingIndicator() + } + + func onRecordingStopped() { + print("Recording stopped") + hideRecordingIndicator() + } + + // Other callbacks... + func onAudioMuted() {} + func onAudioUnMuted() {} + func onVideoPaused() {} + func onVideoResumed() {} + func onScreenShareStarted() {} + func onScreenShareStopped() {} + func onAudioModeChanged(audioModeType: AudioModeType) {} + func onCameraFacingChanged(cameraFacing: CameraFacing) {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addMediaEventsListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeMediaEventsListener:self]; +} + +- (void)onRecordingStarted { + NSLog(@"Recording started"); + [self showRecordingIndicator]; +} + +- (void)onRecordingStopped { + NSLog(@"Recording stopped"); + [self hideRecordingIndicator]; +} + +// Other callbacks... + +@end +``` + + + +## Track Participant Recording + +Monitor when other participants start or stop recording using `ParticipantEventListener`: + + + +```swift +class CallViewController: UIViewController, ParticipantEventListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addParticipantEventListener(self) + } + + func onParticipantStartedRecording(participant: Participant) { + print("\(participant.name ?? "") started recording") + } + + func onParticipantStoppedRecording(participant: Participant) { + print("\(participant.name ?? "") stopped recording") + } + + // Other callbacks... +} +``` + + +```objectivec +- (void)onParticipantStartedRecordingWithParticipant:(Participant *)participant { + NSLog(@"%@ started recording", participant.name); +} + +- (void)onParticipantStoppedRecordingWithParticipant:(Participant *)participant { + NSLog(@"%@ stopped recording", participant.name); +} +``` + + + +## Access Recordings + +Recordings are available after the call ends. You can access them in two ways: + +1. **CometChat Dashboard**: Navigate to **Calls > Call Logs** in your [CometChat Dashboard](https://app.cometchat.com) to view and download recordings. + +2. **Programmatically**: Fetch recordings through [Call Logs](/calls/ios/call-logs). + +## Recording Object + +| Property | Type | Description | +|----------|------|-------------| +| `rid` | String | Unique recording identifier | +| `recordingURL` | String | URL to download/stream the recording | +| `startTime` | Int | Timestamp when recording started | +| `endTime` | Int | Timestamp when recording ended | +| `duration` | Double | Recording duration in seconds | diff --git a/calls/ios/ringing.mdx b/calls/ios/ringing.mdx new file mode 100644 index 00000000..d07d1495 --- /dev/null +++ b/calls/ios/ringing.mdx @@ -0,0 +1,453 @@ +--- +title: "Ringing" +sidebarTitle: "Ringing" +--- + +Implement incoming and outgoing call notifications with accept/reject functionality. Ringing enables real-time call signaling between users, allowing them to initiate calls and respond to incoming call requests. + + +Ringing functionality requires the CometChat Chat SDK to be integrated alongside the Calls SDK. The Chat SDK handles call signaling (initiating, accepting, rejecting calls), while the Calls SDK manages the actual call session. + + +## How Ringing Works + +The ringing flow involves two SDKs working together: + +1. **Chat SDK** - Handles call signaling (initiate, accept, reject, cancel) +2. **Calls SDK** - Manages the actual call session once accepted + +```mermaid +sequenceDiagram + participant Caller + participant ChatSDK + participant Receiver + participant CallsSDK + + Caller->>ChatSDK: initiateCall() + ChatSDK->>Receiver: onIncomingCallReceived + Receiver->>ChatSDK: acceptCall() + ChatSDK-->>Caller: onOutgoingCallAccepted + Caller->>CallsSDK: joinSession() + Receiver->>CallsSDK: joinSession() +``` + +## Initiate a Call + +Use the Chat SDK to initiate a call to a user or group: + + + +```swift +let receiverID = "USER_ID" +let receiverType: CometChat.ReceiverType = .user +let callType: CometChat.CallType = .video + +let call = Call(receiverId: receiverID, callType: callType, receiverType: receiverType) + +CometChat.initiateCall(call: call, onSuccess: { call in + print("Call initiated: \(call?.sessionID ?? "")") + // Show outgoing call UI +}, onError: { error in + print("Call initiation failed: \(error?.errorDescription ?? "")") +}) +``` + + +```objectivec +NSString *receiverID = @"USER_ID"; +CometChatReceiverType receiverType = CometChatReceiverTypeUser; +CometChatCallType callType = CometChatCallTypeVideo; + +Call *call = [[Call alloc] initWithReceiverId:receiverID + callType:callType + receiverType:receiverType]; + +[CometChat initiateCallWithCall:call + onSuccess:^(Call * call) { + NSLog(@"Call initiated: %@", call.sessionID); + // Show outgoing call UI +} onError:^(CometChatException * error) { + NSLog(@"Call initiation failed: %@", error.errorDescription); +}]; +``` + + + +| Parameter | Type | Description | +|-----------|------|-------------| +| `receiverID` | String | UID of the user or GUID of the group to call | +| `receiverType` | ReceiverType | `.user` or `.group` | +| `callType` | CallType | `.video` or `.audio` | + +## Listen for Incoming Calls + +Register a call listener to receive incoming call notifications: + + + +```swift +let listenerID = "UNIQUE_LISTENER_ID" + +CometChat.addCallListener(listenerID, self) + +// Implement CometChatCallDelegate +extension CallViewController: CometChatCallDelegate { + + func onIncomingCallReceived(incomingCall: Call?, error: CometChatException?) { + guard let call = incomingCall else { return } + print("Incoming call from: \(call.callInitiator?.name ?? "")") + // Show incoming call UI with accept/reject options + } + + func onOutgoingCallAccepted(acceptedCall: Call?, error: CometChatException?) { + guard let call = acceptedCall else { return } + print("Call accepted, joining session...") + joinCallSession(sessionId: call.sessionID ?? "") + } + + func onOutgoingCallRejected(rejectedCall: Call?, error: CometChatException?) { + print("Call rejected") + // Dismiss outgoing call UI + } + + func onIncomingCallCancelled(cancelledCall: Call?, error: CometChatException?) { + print("Incoming call cancelled") + // Dismiss incoming call UI + } + + func onCallEndedMessageReceived(endedCall: Call?, error: CometChatException?) { + print("Call ended") + } +} +``` + + +```objectivec +NSString *listenerID = @"UNIQUE_LISTENER_ID"; + +[CometChat addCallListener:listenerID delegate:self]; + +// Implement CometChatCallDelegate +- (void)onIncomingCallReceivedWithIncomingCall:(Call *)incomingCall + error:(CometChatException *)error { + NSLog(@"Incoming call from: %@", incomingCall.callInitiator.name); + // Show incoming call UI with accept/reject options +} + +- (void)onOutgoingCallAcceptedWithAcceptedCall:(Call *)acceptedCall + error:(CometChatException *)error { + NSLog(@"Call accepted, joining session..."); + [self joinCallSessionWithSessionId:acceptedCall.sessionID]; +} + +- (void)onOutgoingCallRejectedWithRejectedCall:(Call *)rejectedCall + error:(CometChatException *)error { + NSLog(@"Call rejected"); + // Dismiss outgoing call UI +} + +- (void)onIncomingCallCancelledWithCancelledCall:(Call *)cancelledCall + error:(CometChatException *)error { + NSLog(@"Incoming call cancelled"); + // Dismiss incoming call UI +} + +- (void)onCallEndedMessageReceivedWithEndedCall:(Call *)endedCall + error:(CometChatException *)error { + NSLog(@"Call ended"); +} +``` + + + +| Callback | Description | +|----------|-------------| +| `onIncomingCallReceived` | A new incoming call is received | +| `onOutgoingCallAccepted` | The receiver accepted your outgoing call | +| `onOutgoingCallRejected` | The receiver rejected your outgoing call | +| `onIncomingCallCancelled` | The caller cancelled the incoming call | +| `onCallEndedMessageReceived` | The call has ended | + + +Remember to remove the call listener when it's no longer needed to prevent memory leaks: +```swift +CometChat.removeCallListener(listenerID) +``` + + +## Accept a Call + +When an incoming call is received, accept it using the Chat SDK: + + + +```swift +func acceptIncomingCall(sessionId: String) { + CometChat.acceptCall(sessionID: sessionId, onSuccess: { call in + print("Call accepted") + self.joinCallSession(sessionId: call?.sessionID ?? "") + }, onError: { error in + print("Accept call failed: \(error?.errorDescription ?? "")") + }) +} +``` + + +```objectivec +- (void)acceptIncomingCallWithSessionId:(NSString *)sessionId { + [CometChat acceptCallWithSessionID:sessionId + onSuccess:^(Call * call) { + NSLog(@"Call accepted"); + [self joinCallSessionWithSessionId:call.sessionID]; + } onError:^(CometChatException * error) { + NSLog(@"Accept call failed: %@", error.errorDescription); + }]; +} +``` + + + +## Reject a Call + +Reject an incoming call: + + + +```swift +func rejectIncomingCall(sessionId: String) { + let status: CometChat.CallStatus = .rejected + + CometChat.rejectCall(sessionID: sessionId, status: status, onSuccess: { call in + print("Call rejected") + // Dismiss incoming call UI + }, onError: { error in + print("Reject call failed: \(error?.errorDescription ?? "")") + }) +} +``` + + +```objectivec +- (void)rejectIncomingCallWithSessionId:(NSString *)sessionId { + [CometChat rejectCallWithSessionID:sessionId + status:CometChatCallStatusRejected + onSuccess:^(Call * call) { + NSLog(@"Call rejected"); + // Dismiss incoming call UI + } onError:^(CometChatException * error) { + NSLog(@"Reject call failed: %@", error.errorDescription); + }]; +} +``` + + + +## Cancel a Call + +Cancel an outgoing call before it's answered: + + + +```swift +func cancelOutgoingCall(sessionId: String) { + let status: CometChat.CallStatus = .cancelled + + CometChat.rejectCall(sessionID: sessionId, status: status, onSuccess: { call in + print("Call cancelled") + // Dismiss outgoing call UI + }, onError: { error in + print("Cancel call failed: \(error?.errorDescription ?? "")") + }) +} +``` + + +```objectivec +- (void)cancelOutgoingCallWithSessionId:(NSString *)sessionId { + [CometChat rejectCallWithSessionID:sessionId + status:CometChatCallStatusCancelled + onSuccess:^(Call * call) { + NSLog(@"Call cancelled"); + // Dismiss outgoing call UI + } onError:^(CometChatException * error) { + NSLog(@"Cancel call failed: %@", error.errorDescription); + }]; +} +``` + + + +## Join the Call Session + +After accepting a call (or when your outgoing call is accepted), join the call session using the Calls SDK: + + + +```swift +func joinCallSession(sessionId: String) { + let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setType(.video) + .build() + + CometChatCalls.joinSession( + sessionID: sessionId, + callSetting: sessionSettings, + container: callViewContainer, + onSuccess: { message in + print("Joined call session") + }, + onError: { error in + print("Failed to join: \(error?.errorDescription ?? "")") + } + ) +} +``` + + +```objectivec +- (void)joinCallSessionWithSessionId:(NSString *)sessionId { + SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + setType:CallTypeVideo] + build]; + + [CometChatCalls joinSessionWithSessionID:sessionId + callSetting:sessionSettings + container:self.callViewContainer + onSuccess:^(NSString * message) { + NSLog(@"Joined call session"); + } onError:^(CometChatCallException * error) { + NSLog(@"Failed to join: %@", error.errorDescription); + }]; +} +``` + + + +## End a Call + +Properly ending a call requires coordination between both SDKs to ensure all participants are notified and call logs are recorded correctly. + + +Always call `CometChat.endCall()` when ending a call. This notifies the other participant and ensures the call is properly logged. Without this, the other user won't know the call has ended and call logs may be incomplete. + + +```mermaid +sequenceDiagram + participant User + participant CallsSDK + participant ChatSDK + participant OtherParticipant + + User->>CallsSDK: leaveSession() + User->>ChatSDK: endCall(sessionId) + ChatSDK->>OtherParticipant: onCallEndedMessageReceived + OtherParticipant->>CallsSDK: leaveSession() +``` + +When using the default call UI, listen for the end call button click using `ButtonClickListener` and call `endCall()`: + + + +```swift +class CallViewController: UIViewController, ButtonClickListener { + + var currentSessionId: String = "" + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addButtonClickListener(self) + } + + func onLeaveSessionButtonClicked() { + endCall(sessionId: currentSessionId) + } + + func endCall(sessionId: String) { + // 1. Leave the call session (Calls SDK) + CallSession.shared.leaveSession() + + // 2. Notify other participants (Chat SDK) + CometChat.endCall(sessionID: sessionId, onSuccess: { call in + print("Call ended successfully") + self.navigationController?.popViewController(animated: true) + }, onError: { error in + print("End call failed: \(error?.errorDescription ?? "")") + self.navigationController?.popViewController(animated: true) + }) + } + + // Other ButtonClickListener callbacks... +} +``` + + +```objectivec +@interface CallViewController () +@property (nonatomic, strong) NSString *currentSessionId; +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addButtonClickListener:self]; +} + +- (void)onLeaveSessionButtonClicked { + [self endCallWithSessionId:self.currentSessionId]; +} + +- (void)endCallWithSessionId:(NSString *)sessionId { + // 1. Leave the call session (Calls SDK) + [[CallSession shared] leaveSession]; + + // 2. Notify other participants (Chat SDK) + [CometChat endCallWithSessionID:sessionId + onSuccess:^(Call * call) { + NSLog(@"Call ended successfully"); + [self.navigationController popViewControllerAnimated:YES]; + } onError:^(CometChatException * error) { + NSLog(@"End call failed: %@", error.errorDescription); + [self.navigationController popViewControllerAnimated:YES]; + }]; +} + +@end +``` + + + +The other participant receives `onCallEndedMessageReceived` callback and should leave the session: + + + +```swift +func onCallEndedMessageReceived(endedCall: Call?, error: CometChatException?) { + CallSession.shared.leaveSession() + navigationController?.popViewController(animated: true) +} +``` + + +```objectivec +- (void)onCallEndedMessageReceivedWithEndedCall:(Call *)endedCall + error:(CometChatException *)error { + [[CallSession shared] leaveSession]; + [self.navigationController popViewControllerAnimated:YES]; +} +``` + + + +## Call Status Values + +| Status | Description | +|--------|-------------| +| `initiated` | Call has been initiated but not yet answered | +| `ongoing` | Call is currently in progress | +| `busy` | Receiver is busy on another call | +| `rejected` | Receiver rejected the call | +| `cancelled` | Caller cancelled before receiver answered | +| `ended` | Call ended normally | +| `missed` | Receiver didn't answer in time | +| `unanswered` | Call was not answered | diff --git a/calls/ios/screen-sharing.mdx b/calls/ios/screen-sharing.mdx new file mode 100644 index 00000000..9b777c0d --- /dev/null +++ b/calls/ios/screen-sharing.mdx @@ -0,0 +1,122 @@ +--- +title: "Screen Sharing" +sidebarTitle: "Screen Sharing" +--- + +View screen shares from other participants during a call. The iOS SDK can receive and display screen shares initiated from web clients. + + +The iOS Calls SDK does not support initiating screen sharing. Screen sharing can only be started from web clients. iOS participants can view shared screens. + + +## How It Works + +When a web participant starts screen sharing: +1. The SDK receives the screen share stream +2. The call layout automatically adjusts to display the shared screen prominently +3. iOS participants can view the shared content + +## Listen for Screen Share Events + +Monitor when participants start or stop screen sharing: + + + +```swift +class CallViewController: UIViewController, ParticipantEventListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addParticipantEventListener(self) + } + + deinit { + CallSession.shared.removeParticipantEventListener(self) + } + + func onParticipantStartedScreenShare(participant: Participant) { + print("\(participant.name ?? "") started screen sharing") + // Layout automatically adjusts to show shared screen + } + + func onParticipantStoppedScreenShare(participant: Participant) { + print("\(participant.name ?? "") stopped screen sharing") + // Layout returns to normal view + } + + // Other callbacks... + func onParticipantJoined(participant: Participant) {} + func onParticipantLeft(participant: Participant) {} + func onParticipantListChanged(participants: [Participant]) {} + func onParticipantAudioMuted(participant: Participant) {} + func onParticipantAudioUnmuted(participant: Participant) {} + func onParticipantVideoPaused(participant: Participant) {} + func onParticipantVideoResumed(participant: Participant) {} + func onParticipantStartedRecording(participant: Participant) {} + func onParticipantStoppedRecording(participant: Participant) {} + func onParticipantHandRaised(participant: Participant) {} + func onParticipantHandLowered(participant: Participant) {} + func onDominantSpeakerChanged(participant: Participant) {} +} +``` + + +```objectivec +@interface CallViewController () +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addParticipantEventListener:self]; +} + +- (void)dealloc { + [[CallSession shared] removeParticipantEventListener:self]; +} + +- (void)onParticipantStartedScreenShareWithParticipant:(Participant *)participant { + NSLog(@"%@ started screen sharing", participant.name); + // Layout automatically adjusts to show shared screen +} + +- (void)onParticipantStoppedScreenShareWithParticipant:(Participant *)participant { + NSLog(@"%@ stopped screen sharing", participant.name); + // Layout returns to normal view +} + +// Other callbacks... + +@end +``` + + + +## Check Screen Share Status + +Use the `isPresenting` property on the `Participant` object to check if someone is sharing their screen: + + + +```swift +func onParticipantListChanged(participants: [Participant]) { + if let presenter = participants.first(where: { $0.isPresenting }) { + print("\(presenter.name ?? "") is currently sharing their screen") + } +} +``` + + +```objectivec +- (void)onParticipantListChangedWithParticipants:(NSArray *)participants { + for (Participant *p in participants) { + if (p.isPresenting) { + NSLog(@"%@ is currently sharing their screen", p.name); + break; + } + } +} +``` + + diff --git a/calls/ios/session-settings.mdx b/calls/ios/session-settings.mdx new file mode 100644 index 00000000..a94d8454 --- /dev/null +++ b/calls/ios/session-settings.mdx @@ -0,0 +1,640 @@ +--- +title: "SessionSettingsBuilder" +sidebarTitle: "SessionSettingsBuilder" +--- + +The `SessionSettingsBuilder` is a powerful configuration tool that allows you to customize every aspect of your call session before participants join. From controlling the initial audio/video state to customizing the UI layout and hiding specific controls, this builder gives you complete control over the call experience. + +Proper session configuration is crucial for creating a seamless user experience tailored to your application's specific needs. + + +These are pre-session configurations that must be set before joining a call. Once configured, pass the `SessionSettings` object to the `joinSession()` method. Settings cannot be changed after the session has started, though many features can be controlled dynamically during the call using call actions. + + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setTitle("Team Meeting") + .setDisplayName("John Doe") + .setType(.video) + .setLayout(.tile) + .startAudioMuted(false) + .startVideoPaused(false) + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[[[[[CometChatCalls sessionSettingsBuilder] + setTitle:@"Team Meeting"] + setDisplayName:@"John Doe"] + setType:CallTypeVideo] + setLayout:LayoutTypeTile] + startAudioMuted:NO] + startVideoPaused:NO] + build]; +``` + + + +## Session Settings + +### Title + +**Method:** `setTitle(_ title: String)` + +Sets the title that appears in the call header. This helps participants identify the purpose or name of the call session. The title is displayed prominently at the top of the call interface. + + + +```swift +.setTitle("Team Meeting") +``` + + +```objectivec +[builder setTitle:@"Team Meeting"] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `title` | String | nil | + +### Display Name + +**Method:** `setDisplayName(_ name: String)` + +Sets the display name that will be shown to other participants in the call. This name appears on your video tile and in the participant list, helping others identify you during the session. + + + +```swift +.setDisplayName("John Doe") +``` + + +```objectivec +[builder setDisplayName:@"John Doe"] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `name` | String | nil | + +### Session Type + +**Method:** `setType(_ type: CallType)` + +Defines the type of call session. Choose `.video` for video calls with camera enabled, or `.audio` for audio-only calls. This setting determines whether video streaming is enabled by default. + + + +```swift +.setType(.video) +``` + + +```objectivec +[builder setType:CallTypeVideo] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `type` | CallType | .video | + + +| Value | Description | +|-------|-------------| +| `.video` | Video call with camera enabled | +| `.audio` | Audio-only call | + + +### Layout Mode + +**Method:** `setLayout(_ layoutType: LayoutType)` + +Sets the initial layout mode for displaying participants. `.tile` shows all participants in a grid, `.spotlight` focuses on the active speaker with others in a sidebar, and `.sidebar` displays the main speaker with participants in a side panel. + + + +```swift +.setLayout(.tile) +``` + + +```objectivec +[builder setLayout:LayoutTypeTile] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `layoutType` | LayoutType | .tile | + + +| Value | Description | +|-------|-------------| +| `.tile` | Grid layout showing all participants equally | +| `.spotlight` | Focus on active speaker with others in sidebar | +| `.sidebar` | Main speaker with participants in a sidebar | + + +### Idle Timeout Period + +**Method:** `setIdleTimeoutPeriod(_ timeoutSeconds: Int)` + +Configures the timeout duration in seconds before automatically ending the session when you're the only participant. This prevents sessions from running indefinitely when others have left. + + + +```swift +.setIdleTimeoutPeriod(300) +``` + + +```objectivec +[builder setIdleTimeoutPeriod:300] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `timeoutSeconds` | Int | 300 | + +### Start Audio Muted + +**Method:** `startAudioMuted(_ muted: Bool)` + +Determines whether the microphone is muted when joining the session. Set to `true` to join with audio muted, requiring users to manually unmute. Useful for large meetings to prevent background noise. + + + +```swift +.startAudioMuted(true) +``` + + +```objectivec +[builder startAudioMuted:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `muted` | Bool | false | + +### Start Video Paused + +**Method:** `startVideoPaused(_ muted: Bool)` + +Controls whether the camera is turned off when joining the session. Set to `true` to join with video disabled, allowing users to enable it when ready. Helpful for privacy or bandwidth considerations. + + + +```swift +.startVideoPaused(true) +``` + + +```objectivec +[builder startVideoPaused:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `muted` | Bool | false | + +### Audio Mode + +**Method:** `setAudioMode(_ audioMode: AudioModeType)` + +Sets the initial audio output device for the call. Options include `.speaker` for loudspeaker, `.earpiece` for phone earpiece, `.bluetooth` for connected Bluetooth devices, or `.headphones` for wired headphones. + + + +```swift +.setAudioMode(.speaker) +``` + + +```objectivec +[builder setAudioMode:AudioModeTypeSpeaker] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `audioMode` | AudioModeType | .speaker | + + +| Value | Description | +|-------|-------------| +| `.speaker` | Device loudspeaker | +| `.earpiece` | Phone earpiece | +| `.bluetooth` | Connected Bluetooth device | +| `.headphones` | Wired headphones | + + +### Initial Camera Facing + +**Method:** `setInitialCameraFacing(_ initialCameraFacing: CameraFacing)` + +Specifies which camera to use when starting the session. Choose `.FRONT` for the front-facing camera (selfie mode) or `.BACK` for the rear camera. Users can switch cameras during the call. + + + +```swift +.setInitialCameraFacing(.FRONT) +``` + + +```objectivec +[builder setInitialCameraFacing:CameraFacingFRONT] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `initialCameraFacing` | CameraFacing | .FRONT | + + +| Value | Description | +|-------|-------------| +| `.FRONT` | Front-facing camera | +| `.BACK` | Rear camera | + + +### Auto Start Recording + +**Method:** `enableAutoStartRecording(_ enabled: Bool)` + +Automatically starts recording the session as soon as it begins. When enabled, recording starts without manual intervention, ensuring the entire session is captured from the start. + + + +```swift +.enableAutoStartRecording(true) +``` + + +```objectivec +[builder enableAutoStartRecording:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `enabled` | Bool | false | + +### Hide Control Panel + +**Method:** `hideControlPanel(_ hidden: Bool)` + +Hides the bottom control bar that contains call action buttons. Set to `true` to remove the control panel entirely, useful for custom UI implementations or view-only modes. + + + +```swift +.hideControlPanel(true) +``` + + +```objectivec +[builder hideControlPanel:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hidden` | Bool | false | + +### Hide Header Panel + +**Method:** `hideHeaderPanel(_ hidden: Bool)` + +Hides the top header bar that displays the call title and session information. Set to `true` to maximize the video viewing area or implement a custom header. + + + +```swift +.hideHeaderPanel(true) +``` + + +```objectivec +[builder hideHeaderPanel:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hidden` | Bool | false | + +### Hide Session Timer + +**Method:** `hideSessionTimer(_ hidden: Bool)` + +Hides the session duration timer that shows how long the call has been active. Set to `true` to remove the timer display from the interface. + + + +```swift +.hideSessionTimer(true) +``` + + +```objectivec +[builder hideSessionTimer:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hidden` | Bool | false | + +### Hide Leave Session Button + +**Method:** `hideLeaveSessionButton(_ hidden: Bool)` + +Hides the button that allows users to leave or end the call. Set to `true` to remove this button, requiring an alternative method to exit the session. + + + +```swift +.hideLeaveSessionButton(true) +``` + + +```objectivec +[builder hideLeaveSessionButton:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hidden` | Bool | false | + +### Hide Toggle Audio Button + +**Method:** `hideToggleAudioButton(_ hidden: Bool)` + +Hides the microphone mute/unmute button from the control panel. Set to `true` to remove audio controls, useful when audio control should be managed programmatically. + + + +```swift +.hideToggleAudioButton(true) +``` + + +```objectivec +[builder hideToggleAudioButton:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hidden` | Bool | false | + +### Hide Toggle Video Button + +**Method:** `hideToggleVideoButton(_ hidden: Bool)` + +Hides the camera on/off button from the control panel. Set to `true` to remove video controls, useful for audio-only calls or custom video control implementations. + + + +```swift +.hideToggleVideoButton(true) +``` + + +```objectivec +[builder hideToggleVideoButton:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hidden` | Bool | false | + +### Hide Switch Camera Button + +**Method:** `hideSwitchCameraButton(_ enabled: Bool)` + +Hides the button that allows switching between front and rear cameras. Set to `true` to remove this control, useful for devices with single cameras or fixed camera requirements. + + + +```swift +.hideSwitchCameraButton(true) +``` + + +```objectivec +[builder hideSwitchCameraButton:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `enabled` | Bool | false | + +### Hide Recording Button + +**Method:** `hideRecordingButton(_ enabled: Bool)` + +Hides the recording start/stop button from the control panel. Set to `false` to show the recording button, allowing users to manually control session recording. + + + +```swift +.hideRecordingButton(false) +``` + + +```objectivec +[builder hideRecordingButton:NO] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `enabled` | Bool | true | + +### Hide Audio Mode Button + +**Method:** `hideAudioModeButton(_ enabled: Bool)` + +Hides the button that toggles between speaker, earpiece, and other audio output modes. Set to `true` to remove audio mode controls from the interface. + + + +```swift +.hideAudioModeButton(true) +``` + + +```objectivec +[builder hideAudioModeButton:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `enabled` | Bool | false | + +### Hide Raise Hand Button + +**Method:** `hideRaiseHandButton(_ enabled: Bool)` + +Hides the raise hand button that participants use to signal they want to speak. Set to `true` to remove this feature from the interface. + + + +```swift +.hideRaiseHandButton(true) +``` + + +```objectivec +[builder hideRaiseHandButton:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `enabled` | Bool | false | + +### Hide Share Invite Button + +**Method:** `hideShareInviteButton(_ enabled: Bool)` + +Hides the button that allows sharing session invite links with others. Set to `false` to show the invite button, enabling easy participant invitation. + + + +```swift +.hideShareInviteButton(false) +``` + + +```objectivec +[builder hideShareInviteButton:NO] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `enabled` | Bool | true | + +### Hide Participant List Button + +**Method:** `hideParticipantListButton(_ hidden: Bool)` + +Hides the button that opens the participant list view. Set to `true` to remove access to the participant list, useful for simplified interfaces. + + + +```swift +.hideParticipantListButton(true) +``` + + +```objectivec +[builder hideParticipantListButton:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hidden` | Bool | false | + +### Hide Change Layout Button + +**Method:** `hideChangeLayoutButton(_ hidden: Bool)` + +Hides the button that allows switching between different layout modes (tile, spotlight, sidebar). Set to `true` to lock the layout to the initial setting. + + + +```swift +.hideChangeLayoutButton(true) +``` + + +```objectivec +[builder hideChangeLayoutButton:YES] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hidden` | Bool | false | + +### Hide Chat Button + +**Method:** `hideChatButton(_ hidden: Bool)` + +Hides the button that opens the in-call chat interface. Set to `false` to show the chat button, enabling text communication during calls. + + + +```swift +.hideChatButton(false) +``` + + +```objectivec +[builder hideChatButton:NO] +``` + + + +| Parameter | Type | Default | +|-----------|------|---------| +| `hidden` | Bool | true | + + +| Enum | Value | Description | +|------|-------|-------------| +| `CallType` | `.video` | Video call with camera enabled | +| | `.audio` | Audio-only call | +| `LayoutType` | `.tile` | Grid layout showing all participants equally | +| | `.spotlight` | Focus on active speaker with others in sidebar | +| | `.sidebar` | Main speaker with participants in a sidebar | +| `AudioModeType` | `.speaker` | Device loudspeaker | +| | `.earpiece` | Phone earpiece | +| | `.bluetooth` | Connected Bluetooth device | +| | `.headphones` | Wired headphones | +| `CameraFacing` | `.FRONT` | Front-facing camera | +| | `.BACK` | Rear camera | + diff --git a/calls/ios/setup.mdx b/calls/ios/setup.mdx new file mode 100644 index 00000000..e66d52dd --- /dev/null +++ b/calls/ios/setup.mdx @@ -0,0 +1,121 @@ +--- +title: "Setup" +sidebarTitle: "Setup" +--- + +This guide walks you through installing the CometChat Calls SDK and initializing it in your iOS application. + +## Add the CometChat Dependency + +### Using CocoaPods + +Add the CometChat Calls SDK to your `Podfile`: + +```ruby +platform :ios, '13.0' +use_frameworks! + +target 'YourApp' do + pod 'CometChatCallsSDK', '~> 5.0.0' +end +``` + +Then run: + +```bash +pod install +``` + +### Using Swift Package Manager + +1. In Xcode, go to **File > Add Package Dependencies** +2. Enter the repository URL: `https://github.com/cometchat/cometchat-calls-sdk-ios` +3. Select the version and add to your target + +## Add Permissions + +Add the required permissions to your `Info.plist`: + +```xml +NSCameraUsageDescription +Camera access is required for video calls +NSMicrophoneUsageDescription +Microphone access is required for voice and video calls +``` + + +iOS requires you to provide a description for why your app needs camera and microphone access. These descriptions are shown to users when requesting permissions. + + +## Enable Background Modes + +For calls to continue when the app is in the background, enable the following background modes in your project's **Signing & Capabilities**: + +1. Select your target in Xcode +2. Go to **Signing & Capabilities** +3. Click **+ Capability** and add **Background Modes** +4. Enable: + - **Audio, AirPlay, and Picture in Picture** + - **Voice over IP** (if using VoIP push notifications) + +## Initialize CometChat Calls + +The `init()` method initializes the SDK with your app credentials. Call this method once when your application starts, typically in your `AppDelegate` or app entry point. + +### CallAppSettings + +The `CallAppSettings` class configures the SDK initialization: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `appId` | String | Yes | Your CometChat App ID | +| `region` | String | Yes | Your app region (`us` or `eu`) | + + + +```swift +import CometChatCallsSDK + +let appId = "APP_ID" // Replace with your App ID +let region = "REGION" // Replace with your Region ("us" or "eu") + +let callAppSettings = CallAppSettingsBuilder() + .setAppId(appId) + .setRegion(region) + .build() + +CometChatCalls(callsAppSettings: callAppSettings, onSuccess: { message in + print("CometChat Calls SDK initialized successfully") +}, onError: { error in + print("CometChat Calls SDK initialization failed: \(error?.errorDescription ?? "")") +}) +``` + + +```objectivec +@import CometChatCallsSDK; + +NSString *appId = @"APP_ID"; // Replace with your App ID +NSString *region = @"REGION"; // Replace with your Region ("us" or "eu") + +CallAppSettings *callAppSettings = [[[CallAppSettingsBuilder alloc] init] + setAppId:appId] + setRegion:region] + build]; + +[[CometChatCalls alloc] initWithCallsAppSettings:callAppSettings + onSuccess:^(NSString * message) { + NSLog(@"CometChat Calls SDK initialized successfully"); + } + onError:^(CometChatCallException * error) { + NSLog(@"CometChat Calls SDK initialization failed: %@", error.errorDescription); + }]; +``` + + + +| Parameter | Description | +|-----------|-------------| +| `callsAppSettings` | Configuration object with App ID and Region | +| `onSuccess` | Closure called on successful initialization | +| `onError` | Closure called if initialization fails | diff --git a/calls/ios/share-invite.mdx b/calls/ios/share-invite.mdx new file mode 100644 index 00000000..b5122804 --- /dev/null +++ b/calls/ios/share-invite.mdx @@ -0,0 +1,591 @@ +--- +title: "Share Invite" +sidebarTitle: "Share Invite" +--- + +Enable participants to invite others to join a call by sharing a meeting link. The share invite button opens the system share sheet, allowing users to send the invite via any messaging app, email, or social media. + +## Overview + +The share invite feature: +- Generates a shareable meeting link with session ID +- Opens iOS's native share sheet +- Works with any app that supports text sharing +- Can be triggered from the default button or custom UI + +```mermaid +flowchart LR + A[Share Button] --> B[Generate Link] + B --> C[Share Sheet] + C --> D[Messages] + C --> E[Mail] + C --> F[AirDrop] + C --> G[Other Apps] +``` + +## Prerequisites + +- CometChat Calls SDK integrated ([Setup](/calls/ios/setup)) +- Active call session ([Join Session](/calls/ios/join-session)) + +--- + +## Step 1: Enable Share Button + +Configure session settings to show the share invite button: + + + +```swift +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .hideShareInviteButton(false) // Show the share button + .build() +``` + + +```objectivec +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + hideShareInviteButton:NO] // Show the share button + build]; +``` + + + +--- + +## Step 2: Handle Share Button Click + +Listen for the share button click using `ButtonClickListener`: + + + +```swift +private func setupShareButtonListener() { + CallSession.shared.addButtonClickListener(self) +} + +extension CallViewController: ButtonClickListener { + + func onShareInviteButtonClicked() { + shareInviteLink() + } +} +``` + + +```objectivec +- (void)setupShareButtonListener { + [[CallSession shared] addButtonClickListener:self]; +} + +- (void)onShareInviteButtonClicked { + [self shareInviteLink]; +} +``` + + + +--- + +## Step 3: Generate and Share Link + +Create the meeting invite URL and open the share sheet: + + + +```swift +private func shareInviteLink() { + let inviteUrl = generateInviteUrl(sessionId: sessionId, meetingName: meetingName) + + let activityItems: [Any] = [ + "Join my meeting: \(meetingName)", + inviteUrl + ] + + let activityVC = UIActivityViewController( + activityItems: activityItems, + applicationActivities: nil + ) + + // For iPad + if let popover = activityVC.popoverPresentationController { + popover.sourceView = view + popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0) + popover.permittedArrowDirections = [] + } + + present(activityVC, animated: true) +} + +private func generateInviteUrl(sessionId: String, meetingName: String) -> URL { + let encodedName = meetingName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? meetingName + + // Replace with your app's deep link or web URL + let urlString = "https://yourapp.com/join?sessionId=\(sessionId)&name=\(encodedName)" + return URL(string: urlString)! +} +``` + + +```objectivec +- (void)shareInviteLink { + NSURL *inviteUrl = [self generateInviteUrlWithSessionId:self.sessionId meetingName:self.meetingName]; + + NSArray *activityItems = @[ + [NSString stringWithFormat:@"Join my meeting: %@", self.meetingName], + inviteUrl + ]; + + UIActivityViewController *activityVC = [[UIActivityViewController alloc] + initWithActivityItems:activityItems + applicationActivities:nil]; + + // For iPad + if (activityVC.popoverPresentationController) { + activityVC.popoverPresentationController.sourceView = self.view; + activityVC.popoverPresentationController.sourceRect = CGRectMake( + CGRectGetMidX(self.view.bounds), + CGRectGetMidY(self.view.bounds), + 0, 0 + ); + activityVC.popoverPresentationController.permittedArrowDirections = 0; + } + + [self presentViewController:activityVC animated:YES completion:nil]; +} + +- (NSURL *)generateInviteUrlWithSessionId:(NSString *)sessionId meetingName:(NSString *)meetingName { + NSString *encodedName = [meetingName stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + + // Replace with your app's deep link or web URL + NSString *urlString = [NSString stringWithFormat:@"https://yourapp.com/join?sessionId=%@&name=%@", sessionId, encodedName]; + return [NSURL URLWithString:urlString]; +} +``` + + + +--- + +## Custom Share Message + +Customize the share message with more details: + + + +```swift +private func shareInviteLink() { + let inviteUrl = generateInviteUrl(sessionId: sessionId, meetingName: meetingName) + + let shareMessage = """ + 📞 Join my meeting: \(meetingName) + + Click the link below to join: + \(inviteUrl.absoluteString) + + Meeting ID: \(sessionId) + """ + + let activityItems: [Any] = [shareMessage] + + let activityVC = UIActivityViewController( + activityItems: activityItems, + applicationActivities: nil + ) + + // For iPad + if let popover = activityVC.popoverPresentationController { + popover.sourceView = view + popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0) + popover.permittedArrowDirections = [] + } + + present(activityVC, animated: true) +} +``` + + +```objectivec +- (void)shareInviteLink { + NSURL *inviteUrl = [self generateInviteUrlWithSessionId:self.sessionId meetingName:self.meetingName]; + + NSString *shareMessage = [NSString stringWithFormat: + @"📞 Join my meeting: %@\n\n" + @"Click the link below to join:\n" + @"%@\n\n" + @"Meeting ID: %@", + self.meetingName, inviteUrl.absoluteString, self.sessionId]; + + NSArray *activityItems = @[shareMessage]; + + UIActivityViewController *activityVC = [[UIActivityViewController alloc] + initWithActivityItems:activityItems + applicationActivities:nil]; + + // For iPad + if (activityVC.popoverPresentationController) { + activityVC.popoverPresentationController.sourceView = self.view; + activityVC.popoverPresentationController.sourceRect = CGRectMake( + CGRectGetMidX(self.view.bounds), + CGRectGetMidY(self.view.bounds), + 0, 0 + ); + activityVC.popoverPresentationController.permittedArrowDirections = 0; + } + + [self presentViewController:activityVC animated:YES completion:nil]; +} +``` + + + +--- + +## Deep Link Handling + +To allow users to join directly from the shared link, implement deep link handling in your app. + +### Configure Universal Links + +Add Associated Domains capability in Xcode and configure your `apple-app-site-association` file on your server. + +### Handle Deep Link + + + +```swift +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, + let url = userActivity.webpageURL else { + return + } + + handleDeepLink(url: url) + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard let url = URLContexts.first?.url else { return } + handleDeepLink(url: url) + } + + private func handleDeepLink(url: URL) { + guard let components = URLComponents(url: url, resolvingAgainstBaseURL: true), + components.path == "/join" else { + return + } + + let queryItems = components.queryItems ?? [] + let sessionId = queryItems.first(where: { $0.name == "sessionId" })?.value + let meetingName = queryItems.first(where: { $0.name == "name" })?.value + + guard let sessionId = sessionId else { return } + + // Check if user is logged in + if CometChat.getLoggedInUser() != nil { + joinCall(sessionId: sessionId, meetingName: meetingName ?? "Meeting") + } else { + // Save params and redirect to login + saveJoinParams(sessionId: sessionId, meetingName: meetingName) + showLoginScreen() + } + } + + private func joinCall(sessionId: String, meetingName: String) { + let callVC = CallViewController() + callVC.sessionId = sessionId + callVC.meetingName = meetingName + callVC.modalPresentationStyle = .fullScreen + + window?.rootViewController?.present(callVC, animated: true) + } + + private func saveJoinParams(sessionId: String, meetingName: String?) { + UserDefaults.standard.set(sessionId, forKey: "pendingSessionId") + UserDefaults.standard.set(meetingName, forKey: "pendingMeetingName") + } + + private func showLoginScreen() { + // Navigate to login + } +} +``` + + +```objectivec +- (void)scene:(UIScene *)scene continueUserActivity:(NSUserActivity *)userActivity { + if (![userActivity.activityType isEqualToString:NSUserActivityTypeBrowsingWeb]) { + return; + } + + NSURL *url = userActivity.webpageURL; + if (url) { + [self handleDeepLink:url]; + } +} + +- (void)scene:(UIScene *)scene openURLContexts:(NSSet *)URLContexts { + UIOpenURLContext *context = URLContexts.anyObject; + if (context) { + [self handleDeepLink:context.URL]; + } +} + +- (void)handleDeepLink:(NSURL *)url { + NSURLComponents *components = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:YES]; + + if (![components.path isEqualToString:@"/join"]) { + return; + } + + NSString *sessionId = nil; + NSString *meetingName = nil; + + for (NSURLQueryItem *item in components.queryItems) { + if ([item.name isEqualToString:@"sessionId"]) { + sessionId = item.value; + } else if ([item.name isEqualToString:@"name"]) { + meetingName = item.value; + } + } + + if (!sessionId) return; + + if ([CometChat getLoggedInUser] != nil) { + [self joinCallWithSessionId:sessionId meetingName:meetingName ?: @"Meeting"]; + } else { + [self saveJoinParamsWithSessionId:sessionId meetingName:meetingName]; + [self showLoginScreen]; + } +} + +- (void)joinCallWithSessionId:(NSString *)sessionId meetingName:(NSString *)meetingName { + CallViewController *callVC = [[CallViewController alloc] init]; + callVC.sessionId = sessionId; + callVC.meetingName = meetingName; + callVC.modalPresentationStyle = UIModalPresentationFullScreen; + + [self.window.rootViewController presentViewController:callVC animated:YES completion:nil]; +} +``` + + + +--- + +## Custom Share Button + +If you want to use a custom share button instead of the default one, hide the default button and implement your own: + + + +```swift +// Hide default share button +let sessionSettings = CometChatCalls.sessionSettingsBuilder + .hideShareInviteButton(true) + .build() + +// Add your custom button +customShareButton.addTarget(self, action: #selector(shareInviteLink), for: .touchUpInside) +``` + + +```objectivec +// Hide default share button +SessionSettings *sessionSettings = [[[CometChatCalls sessionSettingsBuilder] + hideShareInviteButton:YES] + build]; + +// Add your custom button +[customShareButton addTarget:self action:@selector(shareInviteLink) forControlEvents:UIControlEventTouchUpInside]; +``` + + + +--- + +## Complete Example + + + +```swift +class CallViewController: UIViewController { + + var sessionId: String = "" + var meetingName: String = "" + + private let callContainer = UIView() + + override func viewDidLoad() { + super.viewDidLoad() + + setupUI() + setupShareButtonListener() + joinCall() + } + + private func setupUI() { + view.backgroundColor = .black + + callContainer.translatesAutoresizingMaskIntoConstraints = false + view.addSubview(callContainer) + + NSLayoutConstraint.activate([ + callContainer.topAnchor.constraint(equalTo: view.topAnchor), + callContainer.leadingAnchor.constraint(equalTo: view.leadingAnchor), + callContainer.trailingAnchor.constraint(equalTo: view.trailingAnchor), + callContainer.bottomAnchor.constraint(equalTo: view.bottomAnchor) + ]) + } + + private func setupShareButtonListener() { + CallSession.shared.addButtonClickListener(self) + } + + private func joinCall() { + let sessionSettings = CometChatCalls.sessionSettingsBuilder + .setTitle(meetingName) + .hideShareInviteButton(false) + .build() + + CometChatCalls.joinSession( + sessionId: sessionId, + sessionSettings: sessionSettings, + view: callContainer + ) { session in + print("Joined call") + } onError: { error in + print("Join failed: \(error?.errorDescription ?? "")") + } + } + + @objc private func shareInviteLink() { + let inviteUrl = generateInviteUrl(sessionId: sessionId, meetingName: meetingName) + + let shareMessage = "📞 Join my meeting: \(meetingName)\n\n\(inviteUrl.absoluteString)" + + let activityVC = UIActivityViewController( + activityItems: [shareMessage], + applicationActivities: nil + ) + + if let popover = activityVC.popoverPresentationController { + popover.sourceView = view + popover.sourceRect = CGRect(x: view.bounds.midX, y: view.bounds.midY, width: 0, height: 0) + popover.permittedArrowDirections = [] + } + + present(activityVC, animated: true) + } + + private func generateInviteUrl(sessionId: String, meetingName: String) -> URL { + let encodedName = meetingName.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) ?? meetingName + return URL(string: "https://yourapp.com/join?sessionId=\(sessionId)&name=\(encodedName)")! + } +} + +extension CallViewController: ButtonClickListener { + + func onShareInviteButtonClicked() { + shareInviteLink() + } +} +``` + + +```objectivec +@interface CallViewController () +@property (nonatomic, strong) UIView *callContainer; +@property (nonatomic, copy) NSString *sessionId; +@property (nonatomic, copy) NSString *meetingName; +@end + +@implementation CallViewController + +- (void)viewDidLoad { + [super viewDidLoad]; + + [self setupUI]; + [self setupShareButtonListener]; + [self joinCall]; +} + +- (void)setupUI { + self.view.backgroundColor = [UIColor blackColor]; + + self.callContainer = [[UIView alloc] init]; + self.callContainer.translatesAutoresizingMaskIntoConstraints = NO; + [self.view addSubview:self.callContainer]; + + [NSLayoutConstraint activateConstraints:@[ + [self.callContainer.topAnchor constraintEqualToAnchor:self.view.topAnchor], + [self.callContainer.leadingAnchor constraintEqualToAnchor:self.view.leadingAnchor], + [self.callContainer.trailingAnchor constraintEqualToAnchor:self.view.trailingAnchor], + [self.callContainer.bottomAnchor constraintEqualToAnchor:self.view.bottomAnchor] + ]]; +} + +- (void)setupShareButtonListener { + [[CallSession shared] addButtonClickListener:self]; +} + +- (void)joinCall { + SessionSettings *sessionSettings = [[[[CometChatCalls sessionSettingsBuilder] + setTitle:self.meetingName] + hideShareInviteButton:NO] + build]; + + [CometChatCalls joinSessionWithSessionId:self.sessionId + sessionSettings:sessionSettings + view:self.callContainer + onSuccess:^(CallSession *session) { + NSLog(@"Joined call"); + } onError:^(CometChatException *error) { + NSLog(@"Join failed: %@", error.errorDescription); + }]; +} + +- (void)shareInviteLink { + NSURL *inviteUrl = [self generateInviteUrlWithSessionId:self.sessionId meetingName:self.meetingName]; + + NSString *shareMessage = [NSString stringWithFormat:@"📞 Join my meeting: %@\n\n%@", self.meetingName, inviteUrl.absoluteString]; + + UIActivityViewController *activityVC = [[UIActivityViewController alloc] + initWithActivityItems:@[shareMessage] + applicationActivities:nil]; + + if (activityVC.popoverPresentationController) { + activityVC.popoverPresentationController.sourceView = self.view; + activityVC.popoverPresentationController.sourceRect = CGRectMake( + CGRectGetMidX(self.view.bounds), + CGRectGetMidY(self.view.bounds), + 0, 0 + ); + activityVC.popoverPresentationController.permittedArrowDirections = 0; + } + + [self presentViewController:activityVC animated:YES completion:nil]; +} + +- (NSURL *)generateInviteUrlWithSessionId:(NSString *)sessionId meetingName:(NSString *)meetingName { + NSString *encodedName = [meetingName stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet URLQueryAllowedCharacterSet]]; + return [NSURL URLWithString:[NSString stringWithFormat:@"https://yourapp.com/join?sessionId=%@&name=%@", sessionId, encodedName]]; +} + +- (void)onShareInviteButtonClicked { + [self shareInviteLink]; +} + +@end +``` + + + +--- + +## Related Documentation + +- [Events](/calls/ios/events) - Button click events +- [Session Settings](/calls/ios/session-settings) - Configure share button visibility +- [Join Session](/calls/ios/join-session) - Join a call session diff --git a/calls/ios/voip-calling.mdx b/calls/ios/voip-calling.mdx new file mode 100644 index 00000000..24d8eff4 --- /dev/null +++ b/calls/ios/voip-calling.mdx @@ -0,0 +1,663 @@ +--- +title: "VoIP Calling" +sidebarTitle: "VoIP Calling" +--- + +Implement native VoIP calling that works when your app is in the background or killed. This guide shows how to integrate Apple's CallKit framework with CometChat to display system call UI, handle calls from the lock screen, and provide a native calling experience. + +## Overview + +VoIP calling differs from [basic in-app ringing](/calls/ios/ringing) by leveraging Apple's CallKit to: +- Show incoming calls on lock screen with system UI +- Handle calls when app is in background or killed +- Integrate with CarPlay, Bluetooth, and other audio accessories +- Provide consistent call experience across iOS devices + +```mermaid +flowchart TB + subgraph "Incoming Call Flow" + A[Push Notification] --> B[PushKit] + B --> C{App State?} + C -->|Background/Killed| D[CallKit] + C -->|Foreground| E[CometChat CallListener] + D --> F[System Call UI] + E --> G[In-App Call UI] + F --> H{User Action} + G --> H + H -->|Accept| I[CometChat.acceptCall] + H -->|Reject| J[CometChat.rejectCall] + I --> K[CometChatCalls.joinSession] + end +``` + +## Prerequisites + +Before implementing VoIP calling, ensure you have: + +- [CometChat Chat SDK](/sdk/ios/overview) and [Calls SDK](/calls/ios/setup) integrated +- Apple Push Notification service (APNs) VoIP certificate configured +- [Push notifications enabled](/notifications/push-integration) in CometChat Dashboard +- iOS 10.0+ for CallKit support + + +This documentation builds on the [Ringing](/calls/ios/ringing) functionality. Make sure you understand basic call signaling before implementing VoIP. + + +--- + +## Architecture Overview + +The VoIP implementation consists of several components working together: + +| Component | Purpose | +|-----------|---------| +| `PushKit` | Receives VoIP push notifications when app is in background | +| `CXProvider` | CallKit provider that manages call state with the system | +| `CXCallController` | Requests call actions (start, end, hold) from the system | +| `CallManager` | Your custom class that coordinates between CometChat and CallKit | + +--- + +## Step 1: Configure Push Notifications + +VoIP push notifications are essential for receiving incoming calls when your app is not in the foreground. + + +### 1.1 Enable VoIP Capability + +In Xcode, add the following capabilities to your app: +1. Go to your target's "Signing & Capabilities" tab +2. Add "Push Notifications" capability +3. Add "Background Modes" capability and enable: + - Voice over IP + - Background fetch + - Remote notifications + +### 1.2 Register for VoIP Push + + + +```swift +import PushKit + +class AppDelegate: UIResponder, UIApplicationDelegate { + + var voipRegistry: PKPushRegistry! + + func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + + // Initialize CometChat + // CometChat.init(...) + + // Register for VoIP push notifications + registerForVoIPPush() + + return true + } + + private func registerForVoIPPush() { + voipRegistry = PKPushRegistry(queue: DispatchQueue.main) + voipRegistry.delegate = self + voipRegistry.desiredPushTypes = [.voIP] + } +} +``` + + +```objectivec +#import + +@interface AppDelegate () +@property (nonatomic, strong) PKPushRegistry *voipRegistry; +@end + +@implementation AppDelegate + +- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { + + // Initialize CometChat + // [CometChat init:...] + + // Register for VoIP push notifications + [self registerForVoIPPush]; + + return YES; +} + +- (void)registerForVoIPPush { + self.voipRegistry = [[PKPushRegistry alloc] initWithQueue:dispatch_get_main_queue()]; + self.voipRegistry.delegate = self; + self.voipRegistry.desiredPushTypes = [NSSet setWithObject:PKPushTypeVoIP]; +} + +@end +``` + + + +### 1.3 Handle VoIP Push Registration + + + +```swift +extension AppDelegate: PKPushRegistryDelegate { + + func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { + let token = pushCredentials.token.map { String(format: "%02.2hhx", $0) }.joined() + + // Register VoIP token with CometChat + CometChat.registerTokenForPushNotification(token: token) { success in + print("VoIP token registered successfully") + } onError: { error in + print("VoIP token registration failed: \(error?.errorDescription ?? "")") + } + } + + func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { + + guard type == .voIP else { + completion() + return + } + + // Extract call data from payload + let data = payload.dictionaryPayload + guard let sessionId = data["sessionId"] as? String, + let callerName = data["senderName"] as? String, + let callerUid = data["senderUid"] as? String else { + completion() + return + } + + let callType = data["callType"] as? String ?? "video" + let hasVideo = callType == "video" + + // Report incoming call to CallKit + CallManager.shared.reportIncomingCall( + sessionId: sessionId, + callerName: callerName, + callerUid: callerUid, + hasVideo: hasVideo + ) { error in + completion() + } + } +} +``` + + +```objectivec +- (void)pushRegistry:(PKPushRegistry *)registry didUpdatePushCredentials:(PKPushCredentials *)pushCredentials forType:(PKPushType)type { + NSMutableString *token = [NSMutableString string]; + const unsigned char *data = pushCredentials.token.bytes; + for (NSUInteger i = 0; i < pushCredentials.token.length; i++) { + [token appendFormat:@"%02.2hhx", data[i]]; + } + + // Register VoIP token with CometChat + [CometChat registerTokenForPushNotificationWithToken:token onSuccess:^(NSString * success) { + NSLog(@"VoIP token registered successfully"); + } onError:^(CometChatException * error) { + NSLog(@"VoIP token registration failed: %@", error.errorDescription); + }]; +} + +- (void)pushRegistry:(PKPushRegistry *)registry didReceiveIncomingPushWithPayload:(PKPushPayload *)payload forType:(PKPushType)type withCompletionHandler:(void (^)(void))completion { + + if (![type isEqualToString:PKPushTypeVoIP]) { + completion(); + return; + } + + // Extract call data from payload + NSDictionary *data = payload.dictionaryPayload; + NSString *sessionId = data[@"sessionId"]; + NSString *callerName = data[@"senderName"]; + NSString *callerUid = data[@"senderUid"]; + + if (!sessionId || !callerName || !callerUid) { + completion(); + return; + } + + NSString *callType = data[@"callType"] ?: @"video"; + BOOL hasVideo = [callType isEqualToString:@"video"]; + + // Report incoming call to CallKit + [[CallManager shared] reportIncomingCallWithSessionId:sessionId + callerName:callerName + callerUid:callerUid + hasVideo:hasVideo + completion:^(NSError *error) { + completion(); + }]; +} +``` + + + +--- + +## Step 2: Create CallManager + +The CallManager coordinates between CometChat and CallKit: + + + +```swift +import CallKit + +class CallManager: NSObject { + + static let shared = CallManager() + + private let provider: CXProvider + private let callController = CXCallController() + + private var activeCallUUID: UUID? + private var activeSessionId: String? + private var activeCallerUid: String? + + private override init() { + let configuration = CXProviderConfiguration() + configuration.supportsVideo = true + configuration.maximumCallsPerCallGroup = 1 + configuration.supportedHandleTypes = [.generic] + configuration.iconTemplateImageData = UIImage(named: "CallIcon")?.pngData() + + provider = CXProvider(configuration: configuration) + + super.init() + provider.setDelegate(self, queue: nil) + } + + func reportIncomingCall(sessionId: String, callerName: String, callerUid: String, hasVideo: Bool, completion: @escaping (Error?) -> Void) { + let uuid = UUID() + activeCallUUID = uuid + activeSessionId = sessionId + activeCallerUid = callerUid + + let update = CXCallUpdate() + update.remoteHandle = CXHandle(type: .generic, value: callerUid) + update.localizedCallerName = callerName + update.hasVideo = hasVideo + update.supportsGrouping = false + update.supportsUngrouping = false + update.supportsHolding = true + update.supportsDTMF = false + + provider.reportNewIncomingCall(with: uuid, update: update) { error in + if let error = error { + print("Failed to report incoming call: \(error.localizedDescription)") + self.activeCallUUID = nil + self.activeSessionId = nil + } + completion(error) + } + } + + func startOutgoingCall(sessionId: String, receiverName: String, receiverUid: String, hasVideo: Bool) { + let uuid = UUID() + activeCallUUID = uuid + activeSessionId = sessionId + + let handle = CXHandle(type: .generic, value: receiverUid) + let startCallAction = CXStartCallAction(call: uuid, handle: handle) + startCallAction.isVideo = hasVideo + startCallAction.contactIdentifier = receiverName + + let transaction = CXTransaction(action: startCallAction) + callController.request(transaction) { error in + if let error = error { + print("Failed to start call: \(error.localizedDescription)") + } + } + } + + func endCall() { + guard let uuid = activeCallUUID else { return } + + let endCallAction = CXEndCallAction(call: uuid) + let transaction = CXTransaction(action: endCallAction) + + callController.request(transaction) { error in + if let error = error { + print("Failed to end call: \(error.localizedDescription)") + } + } + } + + func reportCallEnded(reason: CXCallEndedReason) { + guard let uuid = activeCallUUID else { return } + provider.reportCall(with: uuid, endedAt: Date(), reason: reason) + activeCallUUID = nil + activeSessionId = nil + } + + func reportCallConnected() { + guard let uuid = activeCallUUID else { return } + provider.reportOutgoingCall(with: uuid, connectedAt: Date()) + } +} +``` + + +```objectivec +#import + +@interface CallManager : NSObject +@property (class, nonatomic, readonly) CallManager *shared; +- (void)reportIncomingCallWithSessionId:(NSString *)sessionId + callerName:(NSString *)callerName + callerUid:(NSString *)callerUid + hasVideo:(BOOL)hasVideo + completion:(void (^)(NSError *))completion; +- (void)startOutgoingCallWithSessionId:(NSString *)sessionId + receiverName:(NSString *)receiverName + receiverUid:(NSString *)receiverUid + hasVideo:(BOOL)hasVideo; +- (void)endCall; +- (void)reportCallEndedWithReason:(CXCallEndedReason)reason; +- (void)reportCallConnected; +@end + +@implementation CallManager { + CXProvider *_provider; + CXCallController *_callController; + NSUUID *_activeCallUUID; + NSString *_activeSessionId; + NSString *_activeCallerUid; +} + +static CallManager *_sharedInstance = nil; + ++ (CallManager *)shared { + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + _sharedInstance = [[CallManager alloc] init]; + }); + return _sharedInstance; +} + +- (instancetype)init { + self = [super init]; + if (self) { + CXProviderConfiguration *configuration = [[CXProviderConfiguration alloc] init]; + configuration.supportsVideo = YES; + configuration.maximumCallsPerCallGroup = 1; + configuration.supportedHandleTypes = [NSSet setWithObject:@(CXHandleTypeGeneric)]; + + _provider = [[CXProvider alloc] initWithConfiguration:configuration]; + [_provider setDelegate:self queue:nil]; + _callController = [[CXCallController alloc] init]; + } + return self; +} + +- (void)reportIncomingCallWithSessionId:(NSString *)sessionId + callerName:(NSString *)callerName + callerUid:(NSString *)callerUid + hasVideo:(BOOL)hasVideo + completion:(void (^)(NSError *))completion { + NSUUID *uuid = [NSUUID UUID]; + _activeCallUUID = uuid; + _activeSessionId = sessionId; + _activeCallerUid = callerUid; + + CXCallUpdate *update = [[CXCallUpdate alloc] init]; + update.remoteHandle = [[CXHandle alloc] initWithType:CXHandleTypeGeneric value:callerUid]; + update.localizedCallerName = callerName; + update.hasVideo = hasVideo; + update.supportsGrouping = NO; + update.supportsUngrouping = NO; + update.supportsHolding = YES; + update.supportsDTMF = NO; + + [_provider reportNewIncomingCallWithUUID:uuid update:update completion:^(NSError * _Nullable error) { + if (error) { + NSLog(@"Failed to report incoming call: %@", error.localizedDescription); + self->_activeCallUUID = nil; + self->_activeSessionId = nil; + } + completion(error); + }]; +} + +@end +``` + + + +--- + +## Step 3: Implement CXProviderDelegate + +Handle CallKit callbacks for user actions: + + + +```swift +extension CallManager: CXProviderDelegate { + + func providerDidReset(_ provider: CXProvider) { + // Clean up any active calls + activeCallUUID = nil + activeSessionId = nil + } + + func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { + // User tapped "Answer" on the call UI + guard let sessionId = activeSessionId else { + action.fail() + return + } + + // Accept the call via CometChat + CometChat.acceptCall(sessionID: sessionId) { call in + action.fulfill() + + // Launch call UI + DispatchQueue.main.async { + self.launchCallViewController(sessionId: sessionId) + } + } onError: { error in + print("Failed to accept call: \(error?.errorDescription ?? "")") + action.fail() + } + } + + func provider(_ provider: CXProvider, perform action: CXEndCallAction) { + // User tapped "Decline" or "End Call" + guard let sessionId = activeSessionId else { + action.fulfill() + return + } + + // Reject or end the call via CometChat + CometChat.rejectCall(sessionID: sessionId, status: .rejected) { call in + action.fulfill() + } onError: { error in + action.fulfill() + } + + activeCallUUID = nil + activeSessionId = nil + } + + func provider(_ provider: CXProvider, perform action: CXSetMutedCallAction) { + if action.isMuted { + CallSession.shared.muteAudio() + } else { + CallSession.shared.unMuteAudio() + } + action.fulfill() + } + + func provider(_ provider: CXProvider, perform action: CXSetHeldCallAction) { + // Handle hold/unhold if needed + action.fulfill() + } + + func provider(_ provider: CXProvider, didActivate audioSession: AVAudioSession) { + // Configure audio session for call + do { + try audioSession.setCategory(.playAndRecord, mode: .voiceChat) + try audioSession.setActive(true) + } catch { + print("Failed to configure audio session: \(error)") + } + } + + func provider(_ provider: CXProvider, didDeactivate audioSession: AVAudioSession) { + // Clean up audio session + do { + try audioSession.setActive(false) + } catch { + print("Failed to deactivate audio session: \(error)") + } + } + + private func launchCallViewController(sessionId: String) { + guard let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let rootViewController = windowScene.windows.first?.rootViewController else { + return + } + + let callVC = CallViewController() + callVC.sessionId = sessionId + callVC.modalPresentationStyle = .fullScreen + + rootViewController.present(callVC, animated: true) + } +} +``` + + +```objectivec +- (void)providerDidReset:(CXProvider *)provider { + _activeCallUUID = nil; + _activeSessionId = nil; +} + +- (void)provider:(CXProvider *)provider performAnswerCallAction:(CXAnswerCallAction *)action { + if (!_activeSessionId) { + [action fail]; + return; + } + + NSString *sessionId = _activeSessionId; + + [CometChat acceptCallWithSessionID:sessionId onSuccess:^(Call * call) { + [action fulfill]; + + dispatch_async(dispatch_get_main_queue(), ^{ + [self launchCallViewControllerWithSessionId:sessionId]; + }); + } onError:^(CometChatException * error) { + NSLog(@"Failed to accept call: %@", error.errorDescription); + [action fail]; + }]; +} + +- (void)provider:(CXProvider *)provider performEndCallAction:(CXEndCallAction *)action { + if (!_activeSessionId) { + [action fulfill]; + return; + } + + [CometChat rejectCallWithSessionID:_activeSessionId status:CometChatCallStatusRejected onSuccess:^(Call * call) { + [action fulfill]; + } onError:^(CometChatException * error) { + [action fulfill]; + }]; + + _activeCallUUID = nil; + _activeSessionId = nil; +} + +- (void)provider:(CXProvider *)provider performSetMutedCallAction:(CXSetMutedCallAction *)action { + if (action.isMuted) { + [[CallSession shared] muteAudio]; + } else { + [[CallSession shared] unMuteAudio]; + } + [action fulfill]; +} +``` + + + +--- + +## Step 4: Handle Call End Events + +Update CallKit when the call ends from the remote side: + + + +```swift +class CallViewController: UIViewController, SessionStatusListener { + + override func viewDidLoad() { + super.viewDidLoad() + CallSession.shared.addSessionStatusListener(self) + } + + func onSessionLeft() { + CallManager.shared.reportCallEnded(reason: .remoteEnded) + dismiss(animated: true) + } + + func onConnectionClosed() { + CallManager.shared.reportCallEnded(reason: .failed) + dismiss(animated: true) + } + + func onSessionJoined() { + CallManager.shared.reportCallConnected() + } + + func onSessionTimedOut() { + CallManager.shared.reportCallEnded(reason: .unanswered) + } + + func onConnectionLost() {} + func onConnectionRestored() {} +} +``` + + +```objectivec +- (void)viewDidLoad { + [super viewDidLoad]; + [[CallSession shared] addSessionStatusListener:self]; +} + +- (void)onSessionLeft { + [[CallManager shared] reportCallEndedWithReason:CXCallEndedReasonRemoteEnded]; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)onConnectionClosed { + [[CallManager shared] reportCallEndedWithReason:CXCallEndedReasonFailed]; + [self dismissViewControllerAnimated:YES completion:nil]; +} + +- (void)onSessionJoined { + [[CallManager shared] reportCallConnected]; +} + +- (void)onSessionTimedOut { + [[CallManager shared] reportCallEndedWithReason:CXCallEndedReasonUnanswered]; +} +``` + + + +--- + +## Related Documentation + +- [Ringing](/calls/ios/ringing) - Basic call signaling +- [Background Handling](/calls/ios/background-handling) - Keep calls alive in background +- [Events](/calls/ios/events) - Session status events diff --git a/calls/javascript/actions.mdx b/calls/javascript/actions.mdx new file mode 100644 index 00000000..116f9523 --- /dev/null +++ b/calls/javascript/actions.mdx @@ -0,0 +1,362 @@ +--- +title: "Actions" +sidebarTitle: "Actions" +--- + +Use call actions to create your own custom controls or trigger call functionality dynamically based on your use case. All actions are called on the `CometChatCalls` class during an active call session. + +## Audio Controls + +### Mute Audio + +Mutes your local microphone, stopping audio transmission to other participants. + +```javascript +CometChatCalls.muteAudio(); +``` + +### Unmute Audio + +Unmutes your local microphone, resuming audio transmission. + +```javascript +CometChatCalls.unMuteAudio(); +``` + +## Video Controls + +### Pause Video + +Turns off your local camera, stopping video transmission. Other participants see your avatar. + +```javascript +CometChatCalls.pauseVideo(); +``` + +### Resume Video + +Turns on your local camera, resuming video transmission. + +```javascript +CometChatCalls.resumeVideo(); +``` + +### Switch Camera + +Toggles between front and back cameras (on supported devices). + +```javascript +CometChatCalls.switchCamera(); +``` + +## Screen Sharing + +### Start Screen Sharing + +Begins sharing your screen with other participants. The browser will prompt the user to select which screen, window, or tab to share. + +```javascript +CometChatCalls.startScreenSharing(); +``` + +### Stop Screen Sharing + +Stops the current screen share. + +```javascript +CometChatCalls.stopScreenSharing(); +``` + +## Recording + +### Start Recording + +Begins server-side recording of the call. All participants are notified. + +```javascript +CometChatCalls.startRecording(); +``` + + +Recording requires the feature to be enabled for your CometChat app. + + +### Stop Recording + +Stops the current recording. The recording is saved and accessible via the dashboard. + +```javascript +CometChatCalls.stopRecording(); +``` + +## Participant Management + +### Mute Participant + +Mutes a specific participant's audio. This is a moderator action. + +```javascript +CometChatCalls.muteParticipant(participantId); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `participantId` | String | The participant's unique identifier | + +### Pause Participant Video + +Pauses a specific participant's video. This is a moderator action. + +```javascript +CometChatCalls.pauseParticipantVideo(participantId); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `participantId` | String | The participant's unique identifier | + +### Pin Participant + +Pins a participant to keep them prominently displayed regardless of who is speaking. + +```javascript +CometChatCalls.pinParticipant(participantId, type); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `participantId` | String | The participant's unique identifier | +| `type` | String | The participant type | + +### Unpin Participant + +Removes the pin, returning to automatic speaker highlighting. + +```javascript +CometChatCalls.unpinParticipant(); +``` + +## Layout + +### Set Layout + +Changes the call layout. Each participant can choose their own layout independently. + +```javascript +CometChatCalls.setLayout("TILE"); +CometChatCalls.setLayout("SIDEBAR"); +CometChatCalls.setLayout("SPOTLIGHT"); +``` + +| Value | Description | +|-------|-------------| +| `TILE` | Grid layout with equally-sized tiles | +| `SIDEBAR` | Main speaker with participants in a sidebar | +| `SPOTLIGHT` | Large view for active speaker, small tiles for others | + +## Raise Hand + +### Raise Hand + +Shows a hand-raised indicator to get attention from other participants. + +```javascript +CometChatCalls.raiseHand(); +``` + +### Lower Hand + +Removes the hand-raised indicator. + +```javascript +CometChatCalls.lowerHand(); +``` + +## Session Control + +### Leave Session + +Ends your participation and disconnects gracefully. The call continues for other participants. + +```javascript +CometChatCalls.leaveSession(); +``` + +## UI Controls + +### Show Settings Dialog + +Opens the settings dialog for audio/video device selection. + +```javascript +CometChatCalls.showSettingsDialog(); +``` + +### Hide Settings Dialog + +Closes the settings dialog. + +```javascript +CometChatCalls.hideSettingsDialog(); +``` + +### Show Virtual Background Dialog + +Opens the virtual background settings dialog. + +```javascript +CometChatCalls.showVirtualBackgroundDialog(); +``` + +### Hide Virtual Background Dialog + +Closes the virtual background dialog. + +```javascript +CometChatCalls.hideVirtualBackgroundDialog(); +``` + +### Set Chat Button Unread Count + +Updates the badge count on the chat button. Pass 0 to hide the badge. + +```javascript +CometChatCalls.setChatButtonUnreadCount(5); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `count` | Number | The unread message count to display | + +## Virtual Background + +### Clear Virtual Background + +Removes any applied virtual background. + +```javascript +CometChatCalls.clearVirtualBackground(); +``` + +### Set Virtual Background Blur Level + +Applies a blur effect to the background with the specified intensity. + +```javascript +CometChatCalls.setVirtualBackgroundBlurLevel(10); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `blurLevel` | Number | The blur intensity level | + +### Set Virtual Background Image + +Sets a custom image as the virtual background. + +```javascript +CometChatCalls.setVirtualBackgroundImage("https://example.com/background.jpg"); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `imageUrl` | String | URL of the background image | + +## Device Management + +### Get Audio Input Devices + +Returns a list of available audio input devices (microphones). + +```javascript +const audioInputDevices = CometChatCalls.getAudioInputDevices(); +console.log(audioInputDevices); +``` + +### Get Audio Output Devices + +Returns a list of available audio output devices (speakers). + +```javascript +const audioOutputDevices = CometChatCalls.getAudioOutputDevices(); +console.log(audioOutputDevices); +``` + +### Get Video Input Devices + +Returns a list of available video input devices (cameras). + +```javascript +const videoInputDevices = CometChatCalls.getVideoInputDevices(); +console.log(videoInputDevices); +``` + +### Get Current Audio Input Device + +Returns the currently selected audio input device. + +```javascript +const currentAudioInput = CometChatCalls.getCurrentAudioInputDevice(); +console.log(currentAudioInput); +``` + +### Get Current Audio Output Device + +Returns the currently selected audio output device. + +```javascript +const currentAudioOutput = CometChatCalls.getCurrentAudioOutputDevice(); +console.log(currentAudioOutput); +``` + +### Get Current Video Input Device + +Returns the currently selected video input device. + +```javascript +const currentVideoInput = CometChatCalls.getCurrentVideoInputDevice(); +console.log(currentVideoInput); +``` + +### Set Audio Input Device + +Sets the audio input device by device ID. + +```javascript +CometChatCalls.setAudioInputDevice(deviceId); +``` + +### Set Audio Output Device + +Sets the audio output device by device ID. + +```javascript +CometChatCalls.setAudioOutputDevice(deviceId); +``` + +### Set Video Input Device + +Sets the video input device by device ID. + +```javascript +CometChatCalls.setVideoInputDevice(deviceId); +``` + +## Picture-in-Picture + +### Enable Picture-in-Picture Layout + +Enables Picture-in-Picture mode for the call. + +```javascript +CometChatCalls.enablePictureInPictureLayout(); +``` + +### Disable Picture-in-Picture Layout + +Disables Picture-in-Picture mode. + +```javascript +CometChatCalls.disablePictureInPictureLayout(); +``` + diff --git a/calls/javascript/angular-integration.mdx b/calls/javascript/angular-integration.mdx new file mode 100644 index 00000000..28b83405 --- /dev/null +++ b/calls/javascript/angular-integration.mdx @@ -0,0 +1,602 @@ +--- +title: "Angular Integration" +sidebarTitle: "Angular" +--- + +This guide walks you through integrating the CometChat Calls SDK into an Angular application. By the end, you'll have a working video call implementation with proper service architecture and lifecycle management. + +## Prerequisites + +Before you begin, ensure you have: +- A CometChat account with an app created ([Sign up](https://app.cometchat.com/signup)) +- Your App ID, Region, and API Key from the CometChat Dashboard +- An Angular 14+ project +- Node.js 16+ installed + +## Step 1: Install the SDK + +Install the CometChat Calls SDK package: + +```bash +npm install @cometchat/calls-sdk-javascript +``` + +## Step 2: Create the Calls Service + +Create a service to manage SDK initialization, authentication, and call operations: + +```typescript +// src/app/services/cometchat-calls.service.ts +import { Injectable } from "@angular/core"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; +import { BehaviorSubject, Observable } from "rxjs"; + +interface User { + uid: string; + name: string; + avatar?: string; +} + +@Injectable({ + providedIn: "root", +}) +export class CometChatCallsService { + private readonly APP_ID = "YOUR_APP_ID"; // Replace with your App ID + private readonly REGION = "YOUR_REGION"; // Replace with your Region + private readonly API_KEY = "YOUR_API_KEY"; // Replace with your API Key + + // Observable state + private isReadySubject = new BehaviorSubject(false); + private userSubject = new BehaviorSubject(null); + private errorSubject = new BehaviorSubject(null); + + isReady$: Observable = this.isReadySubject.asObservable(); + user$: Observable = this.userSubject.asObservable(); + error$: Observable = this.errorSubject.asObservable(); + + /** + * Initialize the SDK and login the user. + * Call this once when your app starts. + */ + async initAndLogin(uid: string): Promise { + if (this.isReadySubject.value) return; + + try { + // Step 1: Initialize the SDK + const initResult = await CometChatCalls.init({ + appId: this.APP_ID, + region: this.REGION, + }); + + if (!initResult.success) { + throw new Error("SDK initialization failed"); + } + + // Step 2: Check if already logged in + let loggedInUser = CometChatCalls.getLoggedInUser(); + + // Step 3: Login if not already logged in + if (!loggedInUser) { + loggedInUser = await CometChatCalls.login(uid, this.API_KEY); + } + + this.userSubject.next(loggedInUser as User); + this.isReadySubject.next(true); + this.errorSubject.next(null); + } catch (err: any) { + console.error("CometChat Calls setup failed:", err); + this.errorSubject.next(err.message || "Setup failed"); + } + } + + /** + * Logout the current user. + */ + async logout(): Promise { + try { + await CometChatCalls.logout(); + this.userSubject.next(null); + this.isReadySubject.next(false); + } catch (err) { + console.error("Logout failed:", err); + } + } + + /** + * Generate a call token for a session. + */ + async generateToken(sessionId: string): Promise<{ token: string }> { + return CometChatCalls.generateToken(sessionId); + } + + /** + * Join a call session. + */ + async joinSession( + token: string, + settings: any, + container: HTMLElement + ): Promise { + return CometChatCalls.joinSession(token, settings, container); + } + + /** + * Leave the current call session. + */ + leaveSession(): void { + CometChatCalls.leaveSession(); + } + + /** + * Add an event listener. + * Returns an unsubscribe function. + */ + addEventListener(event: string, callback: Function): () => void { + return CometChatCalls.addEventListener(event as any, callback as any); + } + + // Audio controls + muteAudio(): void { + CometChatCalls.muteAudio(); + } + + unMuteAudio(): void { + CometChatCalls.unMuteAudio(); + } + + // Video controls + pauseVideo(): void { + CometChatCalls.pauseVideo(); + } + + resumeVideo(): void { + CometChatCalls.resumeVideo(); + } +} +``` + +## Step 3: Initialize in App Component + +Initialize the SDK when your app starts: + +```typescript +// src/app/app.component.ts +import { Component, OnInit } from "@angular/core"; +import { CometChatCallsService } from "./services/cometchat-calls.service"; +import { Observable } from "rxjs"; + +@Component({ + selector: "app-root", + template: ` +
+ Error: {{ error }} +
+
+ Loading... +
+ + `, +}) +export class AppComponent implements OnInit { + isReady$: Observable; + error$: Observable; + + constructor(private callsService: CometChatCallsService) { + this.isReady$ = this.callsService.isReady$; + this.error$ = this.callsService.error$; + } + + ngOnInit(): void { + // In a real app, get this from your authentication system + const currentUserId = "cometchat-uid-1"; + this.callsService.initAndLogin(currentUserId); + } +} +``` + +## Step 4: Create the Call Component + +Build a call component with proper lifecycle management: + +```typescript +// src/app/components/call-screen/call-screen.component.ts +import { + Component, + Input, + Output, + EventEmitter, + OnInit, + OnDestroy, + ViewChild, + ElementRef, +} from "@angular/core"; +import { CometChatCallsService } from "../../services/cometchat-calls.service"; + +@Component({ + selector: "app-call-screen", + template: ` +
+ +
+ + +
+ Joining call... +
+ + +
+

Error: {{ callError }}

+ +
+ + +
+ + + +
+
+ `, + styles: [ + ` + .call-screen { + display: flex; + flex-direction: column; + height: 100vh; + position: relative; + } + .call-container { + flex: 1; + background-color: #1a1a1a; + min-height: 400px; + } + .call-controls { + display: flex; + justify-content: center; + gap: 12px; + padding: 16px; + background-color: #2a2a2a; + } + .call-controls button { + padding: 12px 24px; + border: none; + border-radius: 8px; + font-size: 14px; + cursor: pointer; + } + .call-controls button.active { + background-color: #28a745; + color: white; + } + .call-controls button.muted { + background-color: #dc3545; + color: white; + } + .call-controls .leave-btn { + background-color: #dc3545; + color: white; + } + .call-overlay { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + background: rgba(0, 0, 0, 0.8); + color: white; + padding: 20px; + border-radius: 8px; + text-align: center; + } + `, + ], +}) +export class CallScreenComponent implements OnInit, OnDestroy { + @Input() sessionId!: string; + @Output() callEnded = new EventEmitter(); + @ViewChild("callContainer", { static: true }) callContainer!: ElementRef; + + isJoined = false; + isJoining = false; + isMuted = false; + isVideoOff = false; + callError: string | null = null; + + private unsubscribers: (() => void)[] = []; + + constructor(private callsService: CometChatCallsService) {} + + ngOnInit(): void { + this.joinCall(); + } + + ngOnDestroy(): void { + this.cleanup(); + } + + /** + * Join the call session. + */ + private async joinCall(): Promise { + this.isJoining = true; + this.callError = null; + + try { + // Register event listeners before joining + this.unsubscribers = [ + this.callsService.addEventListener("onSessionJoined", () => { + this.isJoined = true; + this.isJoining = false; + }), + this.callsService.addEventListener("onSessionLeft", () => { + this.isJoined = false; + this.callEnded.emit(); + }), + this.callsService.addEventListener("onAudioMuted", () => { + this.isMuted = true; + }), + this.callsService.addEventListener("onAudioUnMuted", () => { + this.isMuted = false; + }), + this.callsService.addEventListener("onVideoPaused", () => { + this.isVideoOff = true; + }), + this.callsService.addEventListener("onVideoResumed", () => { + this.isVideoOff = false; + }), + ]; + + // Generate a call token for this session + const tokenResult = await this.callsService.generateToken(this.sessionId); + + // Join the call session + const joinResult = await this.callsService.joinSession( + tokenResult.token, + { + sessionType: "VIDEO", + layout: "TILE", + startAudioMuted: false, + startVideoPaused: false, + }, + this.callContainer.nativeElement + ); + + if (joinResult.error) { + throw new Error(joinResult.error.message); + } + } catch (err: any) { + console.error("Failed to join call:", err); + this.callError = err.message || "Failed to join call"; + this.isJoining = false; + } + } + + /** + * Toggle microphone mute state. + */ + toggleAudio(): void { + if (this.isMuted) { + this.callsService.unMuteAudio(); + } else { + this.callsService.muteAudio(); + } + } + + /** + * Toggle camera on/off state. + */ + toggleVideo(): void { + if (this.isVideoOff) { + this.callsService.resumeVideo(); + } else { + this.callsService.pauseVideo(); + } + } + + /** + * Leave the current call session. + */ + leaveCall(): void { + this.callsService.leaveSession(); + } + + /** + * Cleanup event listeners. + */ + private cleanup(): void { + this.unsubscribers.forEach((unsub) => unsub()); + this.unsubscribers = []; + this.callsService.leaveSession(); + } +} +``` + +## Step 5: Create the Call Page + +Create a page component that manages the call flow: + +```typescript +// src/app/pages/call-page/call-page.component.ts +import { Component } from "@angular/core"; +import { CometChatCallsService } from "../../services/cometchat-calls.service"; +import { Observable } from "rxjs"; + +@Component({ + selector: "app-call-page", + template: ` +
+ +
+

CometChat Video Calls

+

Logged in as: {{ (user$ | async)?.name || (user$ | async)?.uid }}

+ +
+ + +
+ +

+ Share the same Session ID with others to join the same call. +

+
+ + + +
+ `, + styles: [ + ` + .call-page { + min-height: 100vh; + } + .pre-call { + max-width: 400px; + margin: 0 auto; + padding: 40px 20px; + text-align: center; + } + .join-form { + margin-top: 30px; + } + .join-form input { + width: 100%; + padding: 12px; + margin-bottom: 12px; + border: 1px solid #ddd; + border-radius: 8px; + font-size: 16px; + box-sizing: border-box; + } + .join-form button { + width: 100%; + padding: 12px; + background-color: #6851d6; + color: white; + border: none; + border-radius: 8px; + font-size: 16px; + cursor: pointer; + } + .join-form button:disabled { + background-color: #ccc; + cursor: not-allowed; + } + .hint { + margin-top: 20px; + color: #666; + font-size: 14px; + } + `, + ], +}) +export class CallPageComponent { + user$: Observable; + sessionId = ""; + isInCall = false; + + constructor(private callsService: CometChatCallsService) { + this.user$ = this.callsService.user$; + } + + startCall(): void { + if (this.sessionId) { + this.isInCall = true; + } + } + + endCall(): void { + this.isInCall = false; + } +} +``` + +## Step 6: Module Configuration + +Add the components to your module: + +```typescript +// src/app/app.module.ts +import { NgModule } from "@angular/core"; +import { BrowserModule } from "@angular/platform-browser"; +import { FormsModule } from "@angular/forms"; +import { RouterModule } from "@angular/router"; + +import { AppComponent } from "./app.component"; +import { CallScreenComponent } from "./components/call-screen/call-screen.component"; +import { CallPageComponent } from "./pages/call-page/call-page.component"; + +@NgModule({ + declarations: [AppComponent, CallScreenComponent, CallPageComponent], + imports: [ + BrowserModule, + FormsModule, + RouterModule.forRoot([ + { path: "", component: CallPageComponent }, + ]), + ], + providers: [], + bootstrap: [AppComponent], +}) +export class AppModule {} +``` + +## Standalone Components (Angular 14+) + +For standalone components without NgModules: + +```typescript +// src/app/components/call-screen/call-screen.component.ts +import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ViewChild, ElementRef } from "@angular/core"; +import { CommonModule } from "@angular/common"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; + +@Component({ + selector: "app-call-screen", + standalone: true, + imports: [CommonModule], + template: `...`, // Same template as above +}) +export class CallScreenComponent implements OnInit, OnDestroy { + // Same implementation, but import CometChatCalls directly + // instead of using the service +} +``` + +## Related Documentation + +For more detailed information on specific topics covered in this guide, refer to the main documentation: + +- [Setup](/calls/javascript/setup) - Detailed SDK installation and initialization +- [Authentication](/calls/javascript/authentication) - Login methods and user management +- [Session Settings](/calls/javascript/session-settings) - All available call configuration options +- [Join Session](/calls/javascript/join-session) - Session joining and token generation +- [Events](/calls/javascript/events) - Complete list of event listeners +- [Actions](/calls/javascript/actions) - All available call control methods +- [Call Layouts](/calls/javascript/call-layouts) - Layout options and customization +- [Participant Management](/calls/javascript/participant-management) - Managing call participants + diff --git a/calls/javascript/authentication.mdx b/calls/javascript/authentication.mdx new file mode 100644 index 00000000..71dc99b3 --- /dev/null +++ b/calls/javascript/authentication.mdx @@ -0,0 +1,153 @@ +--- +title: "Authentication" +sidebarTitle: "Authentication" +--- + +Before users can make or receive calls, they must be authenticated with the CometChat Calls SDK. This guide covers the login and logout methods. + + +**Sample Users** + +CometChat provides 5 test users: `cometchat-uid-1`, `cometchat-uid-2`, `cometchat-uid-3`, `cometchat-uid-4`, and `cometchat-uid-5`. + + +## Check Login Status + +Before calling `login()`, check if a user is already logged in using `getLoggedInUser()`. The SDK maintains the session internally, so you only need to login once per user session. + +```javascript +const loggedInUser = CometChatCalls.getLoggedInUser(); + +if (loggedInUser) { + // User is already logged in + console.log("User already logged in:", loggedInUser.uid); +} else { + // No user logged in, proceed with login +} +``` + +The `getLoggedInUser()` method returns a user object if a user is logged in, or `null` if no session exists. + +## Login with UID and API Key + +This method is suitable for development and testing. For production apps, use [Auth Token login](#login-with-auth-token) instead. + + +**Security Notice** + +Using the API Key directly in client code is not recommended for production. Use Auth Token authentication for enhanced security. + + +```javascript +const uid = "cometchat-uid-1"; // Replace with your user's UID +const apiKey = "API_KEY"; // Replace with your API Key + +if (!CometChatCalls.getLoggedInUser()) { + try { + const user = await CometChatCalls.login(uid, apiKey); + console.log("Login successful:", user.uid); + } catch (error) { + console.error("Login failed:", error.errorDescription); + } +} else { + // User already logged in +} +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `uid` | String | The unique identifier of the user to login | +| `apiKey` | String | Your CometChat API Key | + +## Login with Auth Token + +This is the recommended authentication method for production applications. The Auth Token is generated server-side, keeping your API Key secure. + +### Auth Token Flow + +1. User authenticates with your backend +2. Your backend calls the [CometChat Create Auth Token API](https://api-explorer.cometchat.com/reference/create-authtoken) +3. Your backend returns the Auth Token to the client +4. Client uses the Auth Token to login + +```javascript +const authToken = "AUTH_TOKEN"; // Token received from your backend + +try { + const user = await CometChatCalls.loginWithAuthToken(authToken); + console.log("Login successful:", user.uid); +} catch (error) { + console.error("Login failed:", error.errorDescription); +} +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `authToken` | String | Auth Token generated via CometChat API | + +## User Object + +On successful login, the method returns a user object containing user information: + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | String | Unique identifier of the user | +| `name` | String | Display name of the user | +| `avatar` | String | URL of the user's avatar image | +| `status` | String | User's online status | +| `authToken` | String | The user's auth token | + +## Check User Login Status + +You can verify if a user is currently logged in: + +```javascript +const isLoggedIn = CometChatCalls.isUserLoggedIn(); + +if (isLoggedIn) { + // User is logged in +} else { + // User is not logged in +} +``` + +## Get User Auth Token + +Retrieve the auth token of the currently logged-in user: + +```javascript +const authToken = CometChatCalls.getUserAuthToken(); + +if (authToken) { + console.log("User auth token:", authToken); +} +``` + +## Logout + +Call `logout()` when the user signs out of your application. This clears the local session and disconnects from CometChat services. + +```javascript +try { + const message = await CometChatCalls.logout(); + console.log("Logout successful:", message); +} catch (error) { + console.error("Logout failed:", error.errorDescription); +} +``` + +## Error Handling + +Common authentication errors: + +| Error Code | Description | +|------------|-------------| +| `ERROR_INVALID_UID` | The provided UID is empty or invalid | +| `ERROR_UID_WITH_SPACE` | The UID contains spaces (not allowed) | +| `ERROR_API_KEY_NOT_FOUND` | The API Key is missing or invalid | +| `ERROR_BLANK_AUTHTOKEN` | The Auth Token is empty | +| `ERROR_AUTHTOKEN_WITH_SPACE` | The Auth Token contains spaces (not allowed) | +| `ERROR_LOGIN_IN_PROGRESS` | A login operation is already in progress | +| `ERROR_SDK_NOT_INITIALIZED` | SDK not initialized - call `init()` first | +| `ERROR_NO_USER_LOGGED_IN` | No user is currently logged in (for logout) | + diff --git a/calls/javascript/call-layouts.mdx b/calls/javascript/call-layouts.mdx new file mode 100644 index 00000000..aa9b6c8a --- /dev/null +++ b/calls/javascript/call-layouts.mdx @@ -0,0 +1,76 @@ +--- +title: "Call Layouts" +sidebarTitle: "Call Layouts" +--- + +The CometChat Calls SDK provides three layout modes to display participants during a call. Each participant can independently choose their preferred layout without affecting others. + +## Available Layouts + +| Layout | Description | Best For | +|--------|-------------|----------| +| `TILE` | Grid layout with equally-sized tiles | Group discussions, team meetings | +| `SIDEBAR` | Main speaker with participants in a sidebar | Presentations, webinars | +| `SPOTLIGHT` | Large view for active speaker, small tiles for others | One-on-one calls, focused discussions | + +## Set Initial Layout + +Configure the initial layout when joining a session using the `layout` property in session settings: + +```javascript +const callSettings = { + layout: "TILE", // or "SIDEBAR" or "SPOTLIGHT" + // ... other settings +}; + +await CometChatCalls.joinSession(callToken, callSettings, container); +``` + +## Change Layout During Call + +Change the layout dynamically during an active call: + +```javascript +// Switch to tile layout +CometChatCalls.setLayout("TILE"); + +// Switch to sidebar layout +CometChatCalls.setLayout("SIDEBAR"); + +// Switch to spotlight layout +CometChatCalls.setLayout("SPOTLIGHT"); +``` + +## Listen for Layout Changes + +Monitor when the layout changes: + +```javascript +CometChatCalls.addEventListener("onCallLayoutChanged", (layout) => { + console.log("Layout changed to:", layout); +}); +``` + +## Hide Layout Controls + +To prevent users from changing the layout, hide the layout change button: + +```javascript +const callSettings = { + hideChangeLayoutButton: true, + // ... other settings +}; +``` + +## Layout Constants + +Access layout constants through the SDK: + +```javascript +const layouts = CometChatCalls.constants.LAYOUT; + +console.log(layouts.TILE); // "TILE" +console.log(layouts.SIDEBAR); // "SIDEBAR" +console.log(layouts.SPOTLIGHT); // "SPOTLIGHT" +``` + diff --git a/calls/javascript/call-logs.mdx b/calls/javascript/call-logs.mdx new file mode 100644 index 00000000..d656356d --- /dev/null +++ b/calls/javascript/call-logs.mdx @@ -0,0 +1,51 @@ +--- +title: "Call Logs" +sidebarTitle: "Call Logs" +--- + +Retrieve call history and details using the CometChat REST API. Call logs provide information about past calls including participants, duration, and recording URLs. + +## Retrieving Call Logs + +Call logs are accessed through the CometChat REST API rather than the client SDK. Use the following endpoints: + +### List Calls + +Retrieve a list of calls for your app: + +``` +GET https://{{appId}}.api-{{region}}.cometchat.io/v3/calls +``` + +See the [List Calls API](/calls/api/list-calls) documentation for full details on parameters and response format. + +### Get Call Details + +Retrieve details for a specific call: + +``` +GET https://{{appId}}.api-{{region}}.cometchat.io/v3/calls/{{sessionId}} +``` + +See the [Get Call API](/calls/api/get-call) documentation for full details. + +## Call Log Properties + +| Property | Type | Description | +|----------|------|-------------| +| `sessionId` | String | Unique identifier for the call session | +| `initiator` | Object | User who initiated the call | +| `receiver` | Object | User or group that received the call | +| `type` | String | Call type: `audio` or `video` | +| `status` | String | Call status: `initiated`, `ongoing`, `ended`, etc. | +| `startedAt` | Number | Unix timestamp when the call started | +| `endedAt` | Number | Unix timestamp when the call ended | +| `duration` | Number | Call duration in seconds | +| `participants` | Array | List of participants who joined | +| `recordingUrl` | String | URL to the call recording (if recorded) | + +## Related Documentation + +- [List Calls API](/calls/api/list-calls) +- [Get Call API](/calls/api/get-call) + diff --git a/calls/javascript/custom-control-panel.mdx b/calls/javascript/custom-control-panel.mdx new file mode 100644 index 00000000..0bdae429 --- /dev/null +++ b/calls/javascript/custom-control-panel.mdx @@ -0,0 +1,210 @@ +--- +title: "Custom Control Panel" +sidebarTitle: "Custom Control Panel" +--- + +Build a custom control panel by hiding the default UI and using the SDK's action methods to control call functionality. + +## Hide Default Control Panel + +Hide the built-in control panel when joining a session: + +```javascript +const callSettings = { + hideControlPanel: true, + // ... other settings +}; + +await CometChatCalls.joinSession(callToken, callSettings, container); +``` + +## Build Custom Controls + +With the control panel hidden, use the SDK's action methods to build your own UI: + +### Audio Controls + +```javascript +// Mute/unmute microphone +function toggleAudio(isMuted) { + if (isMuted) { + CometChatCalls.unMuteAudio(); + } else { + CometChatCalls.muteAudio(); + } +} +``` + +### Video Controls + +```javascript +// Toggle camera +function toggleVideo(isPaused) { + if (isPaused) { + CometChatCalls.resumeVideo(); + } else { + CometChatCalls.pauseVideo(); + } +} +``` + +### Screen Sharing + +```javascript +// Toggle screen share +function toggleScreenShare(isSharing) { + if (isSharing) { + CometChatCalls.stopScreenSharing(); + } else { + CometChatCalls.startScreenSharing(); + } +} +``` + +### Leave Session + +```javascript +function leaveCall() { + CometChatCalls.leaveSession(); +} +``` + +## Track State with Events + +Listen to events to keep your custom UI in sync: + +```javascript +let isAudioMuted = false; +let isVideoPaused = false; +let isScreenSharing = false; + +CometChatCalls.addEventListener("onAudioMuted", () => { + isAudioMuted = true; + updateUI(); +}); + +CometChatCalls.addEventListener("onAudioUnMuted", () => { + isAudioMuted = false; + updateUI(); +}); + +CometChatCalls.addEventListener("onVideoPaused", () => { + isVideoPaused = true; + updateUI(); +}); + +CometChatCalls.addEventListener("onVideoResumed", () => { + isVideoPaused = false; + updateUI(); +}); + +CometChatCalls.addEventListener("onScreenShareStarted", () => { + isScreenSharing = true; + updateUI(); +}); + +CometChatCalls.addEventListener("onScreenShareStopped", () => { + isScreenSharing = false; + updateUI(); +}); +``` + +## Hide Individual Buttons + +Instead of hiding the entire control panel, you can hide specific buttons: + +```javascript +const callSettings = { + hideControlPanel: false, + hideLeaveSessionButton: false, + hideToggleAudioButton: false, + hideToggleVideoButton: false, + hideScreenSharingButton: true, + hideRecordingButton: true, + hideRaiseHandButton: true, + hideShareInviteButton: true, + hideChangeLayoutButton: true, + hideParticipantListButton: true, + hideChatButton: true, + hideVirtualBackgroundButton: true, + // ... other settings +}; +``` + +## Complete Example + +```html +
+ +
+ + + + +
+ + +``` + +## Available Actions + +All these methods can be used to build custom controls: + +| Action | Method | +|--------|--------| +| Mute audio | `CometChatCalls.muteAudio()` | +| Unmute audio | `CometChatCalls.unMuteAudio()` | +| Pause video | `CometChatCalls.pauseVideo()` | +| Resume video | `CometChatCalls.resumeVideo()` | +| Start screen share | `CometChatCalls.startScreenSharing()` | +| Stop screen share | `CometChatCalls.stopScreenSharing()` | +| Start recording | `CometChatCalls.startRecording()` | +| Stop recording | `CometChatCalls.stopRecording()` | +| Raise hand | `CometChatCalls.raiseHand()` | +| Lower hand | `CometChatCalls.lowerHand()` | +| Change layout | `CometChatCalls.setLayout(layout)` | +| Leave session | `CometChatCalls.leaveSession()` | + +See [Actions](/calls/javascript/actions) for the complete list of available methods. + diff --git a/calls/javascript/device-management.mdx b/calls/javascript/device-management.mdx new file mode 100644 index 00000000..d6e3bd0b --- /dev/null +++ b/calls/javascript/device-management.mdx @@ -0,0 +1,151 @@ +--- +title: "Device Management" +sidebarTitle: "Device Management" +--- + +Manage audio and video devices during calls, including selecting microphones, speakers, and cameras. + +## Get Available Devices + +### Audio Input Devices (Microphones) + +Get a list of available microphones: + +```javascript +const audioInputDevices = CometChatCalls.getAudioInputDevices(); +console.log(audioInputDevices); +``` + +### Audio Output Devices (Speakers) + +Get a list of available speakers: + +```javascript +const audioOutputDevices = CometChatCalls.getAudioOutputDevices(); +console.log(audioOutputDevices); +``` + +### Video Input Devices (Cameras) + +Get a list of available cameras: + +```javascript +const videoInputDevices = CometChatCalls.getVideoInputDevices(); +console.log(videoInputDevices); +``` + +## Get Current Device + +### Current Audio Input Device + +Get the currently selected microphone: + +```javascript +const currentMic = CometChatCalls.getCurrentAudioInputDevice(); +console.log("Current microphone:", currentMic); +``` + +### Current Audio Output Device + +Get the currently selected speaker: + +```javascript +const currentSpeaker = CometChatCalls.getCurrentAudioOutputDevice(); +console.log("Current speaker:", currentSpeaker); +``` + +### Current Video Input Device + +Get the currently selected camera: + +```javascript +const currentCamera = CometChatCalls.getCurrentVideoInputDevice(); +console.log("Current camera:", currentCamera); +``` + +## Change Device + +### Set Audio Input Device + +Switch to a different microphone: + +```javascript +CometChatCalls.setAudioInputDevice(deviceId); +``` + +### Set Audio Output Device + +Switch to a different speaker: + +```javascript +CometChatCalls.setAudioOutputDevice(deviceId); +``` + +### Set Video Input Device + +Switch to a different camera: + +```javascript +CometChatCalls.setVideoInputDevice(deviceId); +``` + +## Device Change Events + +### Device Selection Changed + +Monitor when the selected device changes: + +```javascript +CometChatCalls.addEventListener("onAudioInputDeviceChanged", (device) => { + console.log("Microphone changed:", device); +}); + +CometChatCalls.addEventListener("onAudioOutputDeviceChanged", (device) => { + console.log("Speaker changed:", device); +}); + +CometChatCalls.addEventListener("onVideoInputDeviceChanged", (device) => { + console.log("Camera changed:", device); +}); +``` + +### Available Devices Changed + +Monitor when devices are connected or disconnected: + +```javascript +CometChatCalls.addEventListener("onAudioInputDevicesChanged", (devices) => { + console.log("Available microphones updated:", devices); +}); + +CometChatCalls.addEventListener("onAudioOutputDevicesChanged", (devices) => { + console.log("Available speakers updated:", devices); +}); + +CometChatCalls.addEventListener("onVideoInputDevicesChanged", (devices) => { + console.log("Available cameras updated:", devices); +}); +``` + +## Settings Dialog + +Open the built-in settings dialog for device selection: + +```javascript +// Show settings dialog +CometChatCalls.showSettingsDialog(); + +// Hide settings dialog +CometChatCalls.hideSettingsDialog(); +``` + +## Device Object + +Each device object contains: + +| Property | Type | Description | +|----------|------|-------------| +| `deviceId` | String | Unique identifier for the device | +| `label` | String | Human-readable device name | +| `kind` | String | Device type (`audioinput`, `audiooutput`, `videoinput`) | + diff --git a/calls/javascript/events.mdx b/calls/javascript/events.mdx new file mode 100644 index 00000000..ab3c45af --- /dev/null +++ b/calls/javascript/events.mdx @@ -0,0 +1,530 @@ +--- +title: "Events" +sidebarTitle: "Events" +--- + +Handle call session events to build responsive UIs. The SDK provides event listeners to monitor session status, participant activities, media changes, button clicks, and layout changes. + +## Adding Event Listeners + +Use the `addEventListener()` method to register event listeners. The method returns an unsubscribe function that you should call to remove the listener when no longer needed. + +```javascript +const unsubscribe = CometChatCalls.addEventListener("eventName", (data) => { + // Handle the event +}); + +// Later, to remove the listener: +unsubscribe(); +``` + +--- + +## Session Events + +Monitor the call session lifecycle including join/leave events and connection status. + +### Session Joined + +Fired when you successfully connect to the session. + +```javascript +CometChatCalls.addEventListener("onSessionJoined", () => { + console.log("Successfully joined the session"); +}); +``` + +### Session Left + +Fired when you leave the session. + +```javascript +CometChatCalls.addEventListener("onSessionLeft", () => { + console.log("Left the session"); +}); +``` + +### Session Timed Out + +Fired when the session ends due to inactivity timeout. + +```javascript +CometChatCalls.addEventListener("onSessionTimedOut", () => { + console.log("Session timed out"); +}); +``` + +### Connection Lost + +Fired when the network connection is interrupted. + +```javascript +CometChatCalls.addEventListener("onConnectionLost", () => { + console.log("Connection lost, attempting to reconnect..."); +}); +``` + +### Connection Restored + +Fired when the connection is restored after being lost. + +```javascript +CometChatCalls.addEventListener("onConnectionRestored", () => { + console.log("Connection restored"); +}); +``` + +### Connection Closed + +Fired when the connection is permanently closed. + +```javascript +CometChatCalls.addEventListener("onConnectionClosed", () => { + console.log("Connection closed"); +}); +``` + +--- + +## Participant Events + +Monitor participant activities including join/leave, audio/video state, hand raise, screen sharing, and recording. + +### Participant Joined + +Fired when a participant joins the call. + +```javascript +CometChatCalls.addEventListener("onParticipantJoined", (participant) => { + console.log("Participant joined:", participant.name); +}); +``` + +### Participant Left + +Fired when a participant leaves the call. + +```javascript +CometChatCalls.addEventListener("onParticipantLeft", (participant) => { + console.log("Participant left:", participant.name); +}); +``` + +### Participant List Changed + +Fired when the participant list is updated. + +```javascript +CometChatCalls.addEventListener("onParticipantListChanged", (participants) => { + console.log("Participants:", participants.length); +}); +``` + +### Participant Audio Muted + +Fired when a participant mutes their microphone. + +```javascript +CometChatCalls.addEventListener("onParticipantAudioMuted", (participant) => { + console.log("Participant muted:", participant.name); +}); +``` + +### Participant Audio Unmuted + +Fired when a participant unmutes their microphone. + +```javascript +CometChatCalls.addEventListener("onParticipantAudioUnmuted", (participant) => { + console.log("Participant unmuted:", participant.name); +}); +``` + +### Participant Video Paused + +Fired when a participant turns off their camera. + +```javascript +CometChatCalls.addEventListener("onParticipantVideoPaused", (participant) => { + console.log("Participant video paused:", participant.name); +}); +``` + +### Participant Video Resumed + +Fired when a participant turns on their camera. + +```javascript +CometChatCalls.addEventListener("onParticipantVideoResumed", (participant) => { + console.log("Participant video resumed:", participant.name); +}); +``` + +### Participant Hand Raised + +Fired when a participant raises their hand. + +```javascript +CometChatCalls.addEventListener("onParticipantHandRaised", (participant) => { + console.log("Participant raised hand:", participant.name); +}); +``` + +### Participant Hand Lowered + +Fired when a participant lowers their hand. + +```javascript +CometChatCalls.addEventListener("onParticipantHandLowered", (participant) => { + console.log("Participant lowered hand:", participant.name); +}); +``` + +### Participant Started Screen Share + +Fired when a participant starts screen sharing. + +```javascript +CometChatCalls.addEventListener("onParticipantStartedScreenShare", (participant) => { + console.log("Participant started screen share:", participant.name); +}); +``` + +### Participant Stopped Screen Share + +Fired when a participant stops screen sharing. + +```javascript +CometChatCalls.addEventListener("onParticipantStoppedScreenShare", (participant) => { + console.log("Participant stopped screen share:", participant.name); +}); +``` + +### Participant Started Recording + +Fired when a participant starts recording. + +```javascript +CometChatCalls.addEventListener("onParticipantStartedRecording", (participant) => { + console.log("Participant started recording:", participant.name); +}); +``` + +### Participant Stopped Recording + +Fired when a participant stops recording. + +```javascript +CometChatCalls.addEventListener("onParticipantStoppedRecording", (participant) => { + console.log("Participant stopped recording:", participant.name); +}); +``` + +### Dominant Speaker Changed + +Fired when the active speaker changes. + +```javascript +CometChatCalls.addEventListener("onDominantSpeakerChanged", (participant) => { + console.log("Dominant speaker:", participant.name); +}); +``` + +--- + +## Media Events + +Monitor your local media state changes including audio/video status, recording, and device changes. + +### Audio Muted + +Fired when your microphone is muted. + +```javascript +CometChatCalls.addEventListener("onAudioMuted", () => { + console.log("Your audio is muted"); +}); +``` + +### Audio Unmuted + +Fired when your microphone is unmuted. + +```javascript +CometChatCalls.addEventListener("onAudioUnMuted", () => { + console.log("Your audio is unmuted"); +}); +``` + +### Video Paused + +Fired when your camera is turned off. + +```javascript +CometChatCalls.addEventListener("onVideoPaused", () => { + console.log("Your video is paused"); +}); +``` + +### Video Resumed + +Fired when your camera is turned on. + +```javascript +CometChatCalls.addEventListener("onVideoResumed", () => { + console.log("Your video is resumed"); +}); +``` + +### Recording Started + +Fired when call recording starts. + +```javascript +CometChatCalls.addEventListener("onRecordingStarted", () => { + console.log("Recording started"); +}); +``` + +### Recording Stopped + +Fired when call recording stops. + +```javascript +CometChatCalls.addEventListener("onRecordingStopped", () => { + console.log("Recording stopped"); +}); +``` + +### Screen Share Started + +Fired when you start screen sharing. + +```javascript +CometChatCalls.addEventListener("onScreenShareStarted", () => { + console.log("Screen sharing started"); +}); +``` + +### Screen Share Stopped + +Fired when you stop screen sharing. + +```javascript +CometChatCalls.addEventListener("onScreenShareStopped", () => { + console.log("Screen sharing stopped"); +}); +``` + +### Audio Input Device Changed + +Fired when the audio input device changes. + +```javascript +CometChatCalls.addEventListener("onAudioInputDeviceChanged", (device) => { + console.log("Audio input device changed:", device); +}); +``` + +### Audio Output Device Changed + +Fired when the audio output device changes. + +```javascript +CometChatCalls.addEventListener("onAudioOutputDeviceChanged", (device) => { + console.log("Audio output device changed:", device); +}); +``` + +### Video Input Device Changed + +Fired when the video input device changes. + +```javascript +CometChatCalls.addEventListener("onVideoInputDeviceChanged", (device) => { + console.log("Video input device changed:", device); +}); +``` + +### Audio Input Devices Changed + +Fired when the list of available audio input devices changes. + +```javascript +CometChatCalls.addEventListener("onAudioInputDevicesChanged", (devices) => { + console.log("Audio input devices updated:", devices); +}); +``` + +### Audio Output Devices Changed + +Fired when the list of available audio output devices changes. + +```javascript +CometChatCalls.addEventListener("onAudioOutputDevicesChanged", (devices) => { + console.log("Audio output devices updated:", devices); +}); +``` + +### Video Input Devices Changed + +Fired when the list of available video input devices changes. + +```javascript +CometChatCalls.addEventListener("onVideoInputDevicesChanged", (devices) => { + console.log("Video input devices updated:", devices); +}); +``` + +--- + +## Button Click Events + +Intercept UI button clicks from the default call interface to add custom behavior or analytics. + +### Leave Session Button Clicked + +Fired when the leave button is clicked. + +```javascript +CometChatCalls.addEventListener("onLeaveSessionButtonClicked", () => { + console.log("Leave button clicked"); +}); +``` + +### Toggle Audio Button Clicked + +Fired when the mute/unmute button is clicked. + +```javascript +CometChatCalls.addEventListener("onToggleAudioButtonClicked", () => { + console.log("Audio toggle button clicked"); +}); +``` + +### Toggle Video Button Clicked + +Fired when the video on/off button is clicked. + +```javascript +CometChatCalls.addEventListener("onToggleVideoButtonClicked", () => { + console.log("Video toggle button clicked"); +}); +``` + +### Raise Hand Button Clicked + +Fired when the raise hand button is clicked. + +```javascript +CometChatCalls.addEventListener("onRaiseHandButtonClicked", () => { + console.log("Raise hand button clicked"); +}); +``` + +### Share Invite Button Clicked + +Fired when the share/invite button is clicked. + +```javascript +CometChatCalls.addEventListener("onShareInviteButtonClicked", () => { + console.log("Share invite button clicked"); +}); +``` + +### Change Layout Button Clicked + +Fired when the layout change button is clicked. + +```javascript +CometChatCalls.addEventListener("onChangeLayoutButtonClicked", () => { + console.log("Change layout button clicked"); +}); +``` + +### Participant List Button Clicked + +Fired when the participant list button is clicked. + +```javascript +CometChatCalls.addEventListener("onParticipantListButtonClicked", () => { + console.log("Participant list button clicked"); +}); +``` + +### Chat Button Clicked + +Fired when the in-call chat button is clicked. + +```javascript +CometChatCalls.addEventListener("onChatButtonClicked", () => { + console.log("Chat button clicked"); +}); +``` + +### Recording Toggle Button Clicked + +Fired when the recording toggle button is clicked. + +```javascript +CometChatCalls.addEventListener("onRecordingToggleButtonClicked", () => { + console.log("Recording toggle button clicked"); +}); +``` + +### Screen Share Button Clicked + +Fired when the screen share button is clicked. + +```javascript +CometChatCalls.addEventListener("onScreenShareButtonClicked", () => { + console.log("Screen share button clicked"); +}); +``` + +--- + +## Layout Events + +Monitor layout changes including layout type switches and participant list visibility. + +### Call Layout Changed + +Fired when the call layout changes. + +```javascript +CometChatCalls.addEventListener("onCallLayoutChanged", (layout) => { + console.log("Layout changed to:", layout); +}); +``` + +### Participant List Visible + +Fired when the participant list panel is opened. + +```javascript +CometChatCalls.addEventListener("onParticipantListVisible", () => { + console.log("Participant list opened"); +}); +``` + +### Participant List Hidden + +Fired when the participant list panel is closed. + +```javascript +CometChatCalls.addEventListener("onParticipantListHidden", () => { + console.log("Participant list closed"); +}); +``` + +--- + +## Participant Object Reference + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | String | Unique identifier (CometChat user ID) | +| `name` | String | Display name | +| `avatar` | String | URL of avatar image | + diff --git a/calls/javascript/idle-timeout.mdx b/calls/javascript/idle-timeout.mdx new file mode 100644 index 00000000..b6216142 --- /dev/null +++ b/calls/javascript/idle-timeout.mdx @@ -0,0 +1,59 @@ +--- +title: "Idle Timeout" +sidebarTitle: "Idle Timeout" +--- + +The idle timeout feature automatically ends a call session when you're the only participant remaining. This prevents sessions from running indefinitely and consuming resources. + +## How It Works + +1. When all other participants leave, a countdown timer starts +2. After the initial timeout period, a prompt is shown to the user +3. If the user doesn't respond, the session ends after the second timeout period + +## Configure Timeout Periods + +Set the timeout periods when joining a session: + +```javascript +const callSettings = { + idleTimeoutPeriodBeforePrompt: 60000, // 60 seconds before showing prompt + idleTimeoutPeriodAfterPrompt: 120000, // 120 seconds after prompt before ending + // ... other settings +}; + +await CometChatCalls.joinSession(callToken, callSettings, container); +``` + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `idleTimeoutPeriodBeforePrompt` | Number | `60000` | Time in milliseconds before showing the timeout prompt | +| `idleTimeoutPeriodAfterPrompt` | Number | `120000` | Time in milliseconds after the prompt before ending the session | + +## Session Timeout Event + +Listen for when the session times out: + +```javascript +CometChatCalls.addEventListener("onSessionTimedOut", () => { + console.log("Session ended due to inactivity"); + // Navigate away or show a message +}); +``` + +## Disable Idle Timeout + +To effectively disable the idle timeout, set very long timeout periods: + +```javascript +const callSettings = { + idleTimeoutPeriodBeforePrompt: 86400000, // 24 hours + idleTimeoutPeriodAfterPrompt: 86400000, // 24 hours + // ... other settings +}; +``` + + +The minimum value for `idleTimeoutPeriodAfterPrompt` is 60 seconds (60000 milliseconds). + + diff --git a/calls/javascript/in-call-chat.mdx b/calls/javascript/in-call-chat.mdx new file mode 100644 index 00000000..62e06d18 --- /dev/null +++ b/calls/javascript/in-call-chat.mdx @@ -0,0 +1,74 @@ +--- +title: "In-Call Chat" +sidebarTitle: "In-Call Chat" +--- + +Enable text messaging during calls by integrating the in-call chat feature. The SDK provides a chat button in the control panel and events to help you build a custom chat experience. + +## Chat Button + +### Show Chat Button + +By default, the chat button is hidden. To show it: + +```javascript +const callSettings = { + hideChatButton: false, + // ... other settings +}; +``` + +### Listen for Chat Button Clicks + +Handle chat button clicks to open your chat interface: + +```javascript +CometChatCalls.addEventListener("onChatButtonClicked", () => { + console.log("Chat button clicked"); + // Open your chat UI +}); +``` + +## Unread Message Badge + +Update the badge count on the chat button to show unread messages: + +```javascript +// Set unread count +CometChatCalls.setChatButtonUnreadCount(5); + +// Clear the badge +CometChatCalls.setChatButtonUnreadCount(0); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `count` | Number | The unread message count to display | + +## Building In-Call Chat + +The Calls SDK provides the UI hooks for in-call chat, but the actual messaging functionality should be implemented using the CometChat Chat SDK or your own messaging solution. + +### Integration with CometChat Chat SDK + +If you're using the CometChat Chat SDK, you can: + +1. Create a group for the call session +2. Use the group ID as the session ID +3. Send and receive messages through the Chat SDK +4. Update the unread badge count based on incoming messages + +```javascript +// Example: Update badge when new message arrives +CometChat.addMessageListener("call-chat-listener", { + onTextMessageReceived: (message) => { + if (message.getReceiverType() === "group" && + message.getReceiverId() === sessionId) { + // Increment unread count + unreadCount++; + CometChatCalls.setChatButtonUnreadCount(unreadCount); + } + } +}); +``` + diff --git a/calls/javascript/ionic-integration.mdx b/calls/javascript/ionic-integration.mdx new file mode 100644 index 00000000..77de909c --- /dev/null +++ b/calls/javascript/ionic-integration.mdx @@ -0,0 +1,1329 @@ +--- +title: "Ionic Integration" +sidebarTitle: "Ionic" +--- + +This guide walks you through integrating the CometChat Calls SDK into an Ionic application. By the end, you'll have a working video call implementation with proper authentication and lifecycle handling. This guide covers Ionic with Angular, React, and Vue. + + +For native mobile features like CallKit, VoIP push notifications, and background handling, consider using the native [iOS](/calls/ios/overview) or [Android](/calls/android/overview) SDKs. + + +## Prerequisites + +Before you begin, ensure you have: +- A CometChat account with an app created ([Sign up](https://app.cometchat.com/signup)) +- Your App ID, Region, and API Key from the CometChat Dashboard +- An Ionic project (Angular, React, or Vue) +- Node.js 16+ installed +- Ionic CLI installed (`npm install -g @ionic/cli`) + +## Step 1: Install the SDK + +Install the CometChat Calls SDK package: + +```bash +npm install @cometchat/calls-sdk-javascript +``` + +## Ionic Angular + +### Step 2: Create the Service + +Create a service that handles SDK initialization, authentication, and call operations. The service waits for the Ionic platform to be ready before initializing: + +```typescript +// src/app/services/cometchat-calls.service.ts +import { Injectable } from "@angular/core"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; +import { Platform } from "@ionic/angular"; +import { BehaviorSubject } from "rxjs"; + +interface User { + uid: string; + name: string; + avatar?: string; +} + +@Injectable({ + providedIn: "root", +}) +export class CometChatCallsService { + private initialized = false; + private _isReady$ = new BehaviorSubject(false); + private _user$ = new BehaviorSubject(null); + private _error$ = new BehaviorSubject(null); + + isReady$ = this._isReady$.asObservable(); + user$ = this._user$.asObservable(); + error$ = this._error$.asObservable(); + + // Replace with your CometChat credentials + private readonly APP_ID = "YOUR_APP_ID"; + private readonly REGION = "YOUR_REGION"; + private readonly API_KEY = "YOUR_API_KEY"; + + constructor(private platform: Platform) {} + + async initAndLogin(uid: string): Promise { + try { + // Wait for Ionic platform to be ready + await this.platform.ready(); + + if (this.initialized) { + return true; + } + + // Step 1: Initialize the SDK + const initResult = await CometChatCalls.init({ + appId: this.APP_ID, + region: this.REGION, + }); + + if (!initResult.success) { + throw new Error("SDK initialization failed"); + } + + // Step 2: Check if already logged in + let loggedInUser = CometChatCalls.getLoggedInUser(); + + // Step 3: Login if not already logged in + if (!loggedInUser) { + loggedInUser = await CometChatCalls.login(uid, this.API_KEY); + } + + this.initialized = true; + this._user$.next(loggedInUser); + this._isReady$.next(true); + return true; + } catch (err: any) { + console.error("CometChat Calls setup failed:", err); + this._error$.next(err.message || "Setup failed"); + return false; + } + } + + getLoggedInUser(): User | null { + return this._user$.value; + } + + async generateToken(sessionId: string) { + return CometChatCalls.generateToken(sessionId); + } + + async joinSession(token: string, settings: any, container: HTMLElement) { + return CometChatCalls.joinSession(token, settings, container); + } + + leaveSession() { + CometChatCalls.leaveSession(); + } + + muteAudio() { + CometChatCalls.muteAudio(); + } + + unMuteAudio() { + CometChatCalls.unMuteAudio(); + } + + pauseVideo() { + CometChatCalls.pauseVideo(); + } + + resumeVideo() { + CometChatCalls.resumeVideo(); + } + + addEventListener(event: string, callback: Function) { + return CometChatCalls.addEventListener(event as any, callback as any); + } +} +``` + + +### Step 3: Initialize in App Component + +Initialize the SDK and login when the app starts: + +```typescript +// src/app/app.component.ts +import { Component, OnInit } from "@angular/core"; +import { CometChatCallsService } from "./services/cometchat-calls.service"; + +@Component({ + selector: "app-root", + templateUrl: "app.component.html", +}) +export class AppComponent implements OnInit { + constructor(private callsService: CometChatCallsService) {} + + ngOnInit() { + // In a real app, get this from your authentication system + const currentUserId = "cometchat-uid-1"; + this.callsService.initAndLogin(currentUserId); + } +} +``` + +### Step 4: Create the Call Page + +Create a call page component that handles joining sessions, media controls, and cleanup: + +```typescript +// src/app/pages/call/call.page.ts +import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from "@angular/core"; +import { ActivatedRoute } from "@angular/router"; +import { NavController } from "@ionic/angular"; +import { CometChatCallsService } from "../../services/cometchat-calls.service"; +import { Subscription } from "rxjs"; + +@Component({ + selector: "app-call", + templateUrl: "./call.page.html", + styleUrls: ["./call.page.scss"], +}) +export class CallPage implements OnInit, OnDestroy { + @ViewChild("callContainer", { static: true }) callContainer!: ElementRef; + + sessionId: string = ""; + isReady = false; + isJoined = false; + isJoining = false; + isMuted = false; + isVideoOff = false; + error: string | null = null; + + private unsubscribers: Function[] = []; + private subscriptions: Subscription[] = []; + + constructor( + private route: ActivatedRoute, + private navCtrl: NavController, + private callsService: CometChatCallsService + ) {} + + ngOnInit() { + this.sessionId = this.route.snapshot.paramMap.get("sessionId") || ""; + + // Subscribe to ready state + this.subscriptions.push( + this.callsService.isReady$.subscribe((ready) => { + this.isReady = ready; + if (ready && this.sessionId) { + this.joinCall(); + } + }), + this.callsService.error$.subscribe((err) => { + this.error = err; + }) + ); + } + + ngOnDestroy() { + this.cleanup(); + this.subscriptions.forEach((sub) => sub.unsubscribe()); + } + + private async joinCall() { + if (!this.callContainer?.nativeElement) return; + + this.isJoining = true; + this.error = null; + + try { + // Register event listeners before joining + this.unsubscribers = [ + this.callsService.addEventListener("onSessionJoined", () => { + this.isJoined = true; + this.isJoining = false; + }), + this.callsService.addEventListener("onSessionLeft", () => { + this.isJoined = false; + this.navCtrl.back(); + }), + this.callsService.addEventListener("onAudioMuted", () => { + this.isMuted = true; + }), + this.callsService.addEventListener("onAudioUnMuted", () => { + this.isMuted = false; + }), + this.callsService.addEventListener("onVideoPaused", () => { + this.isVideoOff = true; + }), + this.callsService.addEventListener("onVideoResumed", () => { + this.isVideoOff = false; + }), + ]; + + // Generate a call token for this session + const tokenResult = await this.callsService.generateToken(this.sessionId); + + // Join the call session + await this.callsService.joinSession( + tokenResult.token, + { + sessionType: "VIDEO", + layout: "TILE", + startAudioMuted: false, + startVideoPaused: false, + }, + this.callContainer.nativeElement + ); + } catch (err: any) { + console.error("Failed to join call:", err); + this.error = err.message || "Failed to join call"; + this.isJoining = false; + } + } + + toggleAudio() { + this.isMuted ? this.callsService.unMuteAudio() : this.callsService.muteAudio(); + } + + toggleVideo() { + this.isVideoOff ? this.callsService.resumeVideo() : this.callsService.pauseVideo(); + } + + leaveCall() { + this.callsService.leaveSession(); + } + + private cleanup() { + this.unsubscribers.forEach((unsub) => unsub()); + this.unsubscribers = []; + this.callsService.leaveSession(); + } +} +``` + + +### Step 5: Create the Call Page Template + +Create the HTML template for the call page with video container and controls: + +```html + + + +
+ +

Initializing...

+
+ + +
+ +

{{ error }}

+ Retry +
+ + +
+ + +
+ +

Joining call...

+
+ + +
+ + + + + + + + + + + +
+
+``` + +```scss +/* src/app/pages/call/call.page.scss */ +.call-container { + width: 100%; + height: calc(100% - 80px); + background-color: #1a1a1a; +} + +.call-controls { + display: flex; + justify-content: center; + gap: 16px; + padding: 16px; + background-color: #f5f5f5; +} + +.loading-container, +.error-container, +.joining-overlay { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + gap: 16px; +} + +.joining-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: rgba(0, 0, 0, 0.7); + color: white; + z-index: 100; +} +``` + +### Step 6: Create the Home Page + +Create a home page where users can enter a session ID and join a call: + +```typescript +// src/app/pages/home/home.page.ts +import { Component } from "@angular/core"; +import { Router } from "@angular/router"; +import { CometChatCallsService } from "../../services/cometchat-calls.service"; + +@Component({ + selector: "app-home", + templateUrl: "./home.page.html", +}) +export class HomePage { + sessionId = ""; + isReady$ = this.callsService.isReady$; + user$ = this.callsService.user$; + error$ = this.callsService.error$; + + constructor( + private router: Router, + private callsService: CometChatCallsService + ) {} + + joinCall() { + if (this.sessionId) { + this.router.navigate(["/call", this.sessionId]); + } + } +} +``` + +```html + + + + CometChat Calls + + + + +
+ {{ error }} +
+ +
+ +

Loading...

+
+ +
+

+ Logged in as: {{ user.name || user.uid }} +

+ + + Session ID + + + + + Join Call + +
+
+``` + + +## Ionic React + +### Step 2: Create the Provider + +Create a context provider that handles SDK initialization and authentication: + +```tsx +// src/providers/CometChatCallsProvider.tsx +import { createContext, useContext, useEffect, useState, ReactNode } from "react"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; +import { isPlatform } from "@ionic/react"; + +interface User { + uid: string; + name: string; + avatar?: string; +} + +interface CometChatCallsContextType { + isReady: boolean; + user: User | null; + error: string | null; +} + +const CometChatCallsContext = createContext({ + isReady: false, + user: null, + error: null, +}); + +// Replace with your CometChat credentials +const APP_ID = "YOUR_APP_ID"; +const REGION = "YOUR_REGION"; +const API_KEY = "YOUR_API_KEY"; + +interface ProviderProps { + children: ReactNode; + uid: string; +} + +export function CometChatCallsProvider({ children, uid }: ProviderProps) { + const [isReady, setIsReady] = useState(false); + const [user, setUser] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + async function initAndLogin() { + try { + // Step 1: Initialize the SDK + const initResult = await CometChatCalls.init({ + appId: APP_ID, + region: REGION, + }); + + if (!initResult.success) { + throw new Error("SDK initialization failed"); + } + + // Step 2: Check if already logged in + let loggedInUser = CometChatCalls.getLoggedInUser(); + + // Step 3: Login if not already logged in + if (!loggedInUser) { + loggedInUser = await CometChatCalls.login(uid, API_KEY); + } + + setUser(loggedInUser); + setIsReady(true); + } catch (err: any) { + console.error("CometChat Calls setup failed:", err); + setError(err.message || "Setup failed"); + } + } + + if (uid) { + initAndLogin(); + } + }, [uid]); + + return ( + + {children} + + ); +} + +export function useCometChatCalls(): CometChatCallsContextType { + return useContext(CometChatCallsContext); +} +``` + +### Step 3: Wrap Your App + +Add the provider to your app's root component: + +```tsx +// src/App.tsx +import { IonApp, IonRouterOutlet, setupIonicReact } from "@ionic/react"; +import { IonReactRouter } from "@ionic/react-router"; +import { Route } from "react-router-dom"; +import { CometChatCallsProvider } from "./providers/CometChatCallsProvider"; +import HomePage from "./pages/Home"; +import CallPage from "./pages/Call"; + +setupIonicReact(); + +const App: React.FC = () => { + // In a real app, get this from your authentication system + const currentUserId = "cometchat-uid-1"; + + return ( + + + + + + + + + + + ); +}; + +export default App; +``` + +### Step 4: Create the Call Page + +Create a call page that handles joining sessions, media controls, and cleanup: + +```tsx +// src/pages/Call.tsx +import { useEffect, useRef, useState } from "react"; +import { + IonContent, + IonPage, + IonButton, + IonIcon, + IonSpinner, + useIonRouter +} from "@ionic/react"; +import { mic, micOff, videocam, videocamOff, call } from "ionicons/icons"; +import { useParams } from "react-router-dom"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; +import { useCometChatCalls } from "../providers/CometChatCallsProvider"; + +const CallPage: React.FC = () => { + const { sessionId } = useParams<{ sessionId: string }>(); + const { isReady, error: initError } = useCometChatCalls(); + const router = useIonRouter(); + const containerRef = useRef(null); + + // Call state + const [isJoined, setIsJoined] = useState(false); + const [isJoining, setIsJoining] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [isVideoOff, setIsVideoOff] = useState(false); + const [error, setError] = useState(null); + + const unsubscribersRef = useRef([]); + + useEffect(() => { + if (!isReady || !containerRef.current || !sessionId) return; + + async function joinCall() { + setIsJoining(true); + setError(null); + + try { + // Register event listeners before joining + unsubscribersRef.current = [ + CometChatCalls.addEventListener("onSessionJoined", () => { + setIsJoined(true); + setIsJoining(false); + }), + CometChatCalls.addEventListener("onSessionLeft", () => { + setIsJoined(false); + router.goBack(); + }), + CometChatCalls.addEventListener("onAudioMuted", () => setIsMuted(true)), + CometChatCalls.addEventListener("onAudioUnMuted", () => setIsMuted(false)), + CometChatCalls.addEventListener("onVideoPaused", () => setIsVideoOff(true)), + CometChatCalls.addEventListener("onVideoResumed", () => setIsVideoOff(false)), + ]; + + // Generate a call token for this session + const tokenResult = await CometChatCalls.generateToken(sessionId); + + // Join the call session + await CometChatCalls.joinSession( + tokenResult.token, + { + sessionType: "VIDEO", + layout: "TILE", + startAudioMuted: false, + startVideoPaused: false, + }, + containerRef.current! + ); + } catch (err: any) { + console.error("Failed to join call:", err); + setError(err.message || "Failed to join call"); + setIsJoining(false); + } + } + + joinCall(); + + return () => { + unsubscribersRef.current.forEach((unsub) => unsub()); + unsubscribersRef.current = []; + CometChatCalls.leaveSession(); + }; + }, [isReady, sessionId, router]); + + // Control handlers + const toggleAudio = () => { + isMuted ? CometChatCalls.unMuteAudio() : CometChatCalls.muteAudio(); + }; + + const toggleVideo = () => { + isVideoOff ? CometChatCalls.resumeVideo() : CometChatCalls.pauseVideo(); + }; + + const leaveCall = () => { + CometChatCalls.leaveSession(); + }; + + // Loading state + if (!isReady) { + return ( + + + +

Initializing...

+
+
+ ); + } + + // Error state + if (error || initError) { + return ( + + +

+ Error: {error || initError} +

+ window.location.reload()}>Retry +
+
+ ); + } + + return ( + + + {/* Video container - SDK renders the call UI here */} +
+ + {/* Joining overlay */} + {isJoining && ( +
+ +

Joining call...

+
+ )} + + {/* Call controls */} + {isJoined && ( +
+ + + + + + + + + +
+ )} + + + ); +}; + +export default CallPage; +``` + + +### Step 5: Create the Home Page + +Create a home page where users can enter a session ID and join a call: + +```tsx +// src/pages/Home.tsx +import { useState } from "react"; +import { + IonContent, + IonPage, + IonHeader, + IonToolbar, + IonTitle, + IonItem, + IonLabel, + IonInput, + IonButton, + IonSpinner, + IonText, + useIonRouter +} from "@ionic/react"; +import { useCometChatCalls } from "../providers/CometChatCallsProvider"; + +const HomePage: React.FC = () => { + const { isReady, user, error } = useCometChatCalls(); + const router = useIonRouter(); + const [sessionId, setSessionId] = useState(""); + + const joinCall = () => { + if (sessionId) { + router.push(`/call/${sessionId}`); + } + }; + + return ( + + + + CometChat Calls + + + + {error && ( + +

{error}

+
+ )} + + {!isReady ? ( +
+ +

Loading...

+
+ ) : ( + <> +

Logged in as: {user?.name || user?.uid}

+ + + Session ID + setSessionId(e.detail.value || "")} + placeholder="Enter Session ID" + /> + + + + Join Call + + + )} +
+
+ ); +}; + +export default HomePage; +``` + +## Ionic Vue + +### Step 2: Create the Composable + +Create a composable that handles SDK initialization and authentication: + +```typescript +// src/composables/useCometChatCalls.ts +import { ref, readonly } from "vue"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; + +interface User { + uid: string; + name: string; + avatar?: string; +} + +// Replace with your CometChat credentials +const APP_ID = "YOUR_APP_ID"; +const REGION = "YOUR_REGION"; +const API_KEY = "YOUR_API_KEY"; + +// Shared state across all components +const isReady = ref(false); +const user = ref(null); +const error = ref(null); +const initialized = ref(false); + +export function useCometChatCalls() { + async function initAndLogin(uid: string): Promise { + if (initialized.value) { + return isReady.value; + } + + try { + // Step 1: Initialize the SDK + const initResult = await CometChatCalls.init({ + appId: APP_ID, + region: REGION, + }); + + if (!initResult.success) { + throw new Error("SDK initialization failed"); + } + + // Step 2: Check if already logged in + let loggedInUser = CometChatCalls.getLoggedInUser(); + + // Step 3: Login if not already logged in + if (!loggedInUser) { + loggedInUser = await CometChatCalls.login(uid, API_KEY); + } + + user.value = loggedInUser; + isReady.value = true; + initialized.value = true; + return true; + } catch (err: any) { + console.error("CometChat Calls setup failed:", err); + error.value = err.message || "Setup failed"; + return false; + } + } + + return { + isReady: readonly(isReady), + user: readonly(user), + error: readonly(error), + initAndLogin, + }; +} +``` + +### Step 3: Initialize in App Component + +Initialize the SDK and login when the app starts: + +```vue + + + + +``` + +### Step 4: Create the Call Page + +Create a call page that handles joining sessions, media controls, and cleanup: + +```vue + + + + + + +``` + + +### Step 5: Create the Home Page + +Create a home page where users can enter a session ID and join a call: + +```vue + + + + +``` + +### Step 6: Configure Routes + +Set up the router with the home and call pages: + +```typescript +// src/router/index.ts +import { createRouter, createWebHistory } from "@ionic/vue-router"; +import { RouteRecordRaw } from "vue-router"; +import HomePage from "../views/HomePage.vue"; +import CallPage from "../views/CallPage.vue"; + +const routes: Array = [ + { + path: "/", + component: HomePage, + }, + { + path: "/call/:sessionId", + component: CallPage, + }, +]; + +const router = createRouter({ + history: createWebHistory(import.meta.env.BASE_URL), + routes, +}); + +export default router; +``` + +## Related Documentation + +For more detailed information on specific topics covered in this guide, refer to the main documentation: + +- [Setup](/calls/javascript/setup) - Detailed SDK installation and initialization +- [Authentication](/calls/javascript/authentication) - Login methods and user management +- [Session Settings](/calls/javascript/session-settings) - All available call configuration options +- [Join Session](/calls/javascript/join-session) - Session joining and token generation +- [Events](/calls/javascript/events) - Complete list of event listeners +- [Actions](/calls/javascript/actions) - All available call control methods +- [Call Layouts](/calls/javascript/call-layouts) - Layout options and customization +- [Participant Management](/calls/javascript/participant-management) - Managing call participants diff --git a/calls/javascript/join-session.mdx b/calls/javascript/join-session.mdx new file mode 100644 index 00000000..b4e88361 --- /dev/null +++ b/calls/javascript/join-session.mdx @@ -0,0 +1,146 @@ +--- +title: "Join Session" +sidebarTitle: "Join Session" +--- + +Join a call session using one of two approaches: the quick start method with a session ID, or the advanced flow with manual token generation for more control. + +## Overview + +The CometChat Calls SDK provides two ways to join a session: + +| Approach | Best For | Complexity | +|----------|----------|------------| +| **Join with Token** | Most use cases - recommended approach | Low - Two-step process | +| **Generate Token Separately** | Custom token management, pre-generation, caching | Medium - Separate token handling | + + +Both approaches require a container element in your HTML where the call interface will be rendered. + + +## Container Setup + +Add a container element to your HTML where the call interface will be rendered: + +```html +
+``` + +The call UI will be dynamically rendered inside this container when you join the session. + +## Generate Token + +Generate a call token for the session. Each token is unique to a specific session and user combination. + +```javascript +const sessionId = "SESSION_ID"; + +try { + const result = await CometChatCalls.generateToken(sessionId); + console.log("Token generated:", result.token); + // Use the token to join the session +} catch (error) { + console.error("Token generation failed:", error.errorDescription); +} +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `sessionId` | String | Unique identifier for the call session | + +The method returns an object with: + +| Property | Type | Description | +|----------|------|-------------| +| `token` | String | The generated call token | + + +The `generateToken()` method uses the auth token of the currently logged-in user. Ensure a user is logged in before calling this method. + + +## Join Session + +Use the generated token to join the session along with your call settings and container element. + +```javascript +const container = document.getElementById("call-container"); + +const callSettings = { + sessionType: "VIDEO", + layout: "TILE", + startAudioMuted: false, + startVideoPaused: false, +}; + +const result = await CometChatCalls.joinSession(callToken, callSettings, container); + +if (result.error) { + console.error("Failed to join session:", result.error); +} else { + console.log("Joined session successfully"); +} +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `callToken` | String | Previously generated call token | +| `callSettings` | Object | Configuration for the session (see [Session Settings](/calls/javascript/session-settings)) | +| `container` | HTMLElement | Container element for the call UI | + +The method returns an object with: + +| Property | Type | Description | +|----------|------|-------------| +| `data` | undefined | Undefined on success | +| `error` | Object \| null | Error details if join failed | + +## Complete Example + +```javascript +const sessionId = "SESSION_ID"; +const container = document.getElementById("call-container"); + +// Step 1: Generate token +try { + const tokenResult = await CometChatCalls.generateToken(sessionId); + + // Step 2: Configure session settings + const callSettings = { + sessionType: "VIDEO", + layout: "TILE", + startAudioMuted: false, + startVideoPaused: false, + }; + + // Step 3: Join session + const joinResult = await CometChatCalls.joinSession( + tokenResult.token, + callSettings, + container + ); + + if (joinResult.error) { + console.error("Failed to join session:", joinResult.error); + } else { + console.log("Joined session successfully"); + } +} catch (error) { + console.error("Error:", error); +} +``` + + +All participants joining the same call must use the same session ID. + + +## Error Handling + +Common errors when joining a session: + +| Error Code | Description | +|------------|-------------| +| `ERROR_SDK_NOT_INITIALIZED` | SDK not initialized - call `init()` first | +| `ERROR_AUTH_TOKEN_MISSING` | User not logged in or auth token invalid | +| `ERROR_SESSION_ID_MISSING` | Session ID is null or empty | +| `VALIDATION_ERROR` | Invalid call settings | + diff --git a/calls/javascript/nextjs-integration.mdx b/calls/javascript/nextjs-integration.mdx new file mode 100644 index 00000000..d59b3721 --- /dev/null +++ b/calls/javascript/nextjs-integration.mdx @@ -0,0 +1,557 @@ +--- +title: "Next.js Integration" +sidebarTitle: "Next.js" +--- + +This guide walks you through integrating the CometChat Calls SDK into a Next.js application. By the end, you'll have a working video call implementation with proper server-side rendering handling and authentication. + +## Prerequisites + +Before you begin, ensure you have: +- A CometChat account with an app created ([Sign up](https://app.cometchat.com/signup)) +- Your App ID, Region, and API Key from the CometChat Dashboard +- A Next.js project (App Router or Pages Router) +- Node.js 16+ installed + +## Important: Client-Side Only + +The CometChat Calls SDK uses browser APIs (WebRTC, DOM) that are not available during server-side rendering. You must ensure the SDK only loads and runs on the client side. This guide shows you how to handle this properly with both the App Router and Pages Router. + +## Step 1: Install the SDK + +Install the CometChat Calls SDK package: + +```bash +npm install @cometchat/calls-sdk-javascript +``` + +## Step 2: Configure Environment Variables + +Add your CometChat credentials to `.env.local`. The `NEXT_PUBLIC_` prefix makes these variables available in the browser: + +```env +NEXT_PUBLIC_COMETCHAT_APP_ID=your_app_id +NEXT_PUBLIC_COMETCHAT_REGION=us +NEXT_PUBLIC_COMETCHAT_API_KEY=your_api_key +``` + + +Never expose your API Key in production client-side code. Use a backend service to generate auth tokens securely. The API Key approach shown here is for development and testing only. + + +## Step 3: Create the Provider + +Create a context provider that handles SDK initialization and user authentication. The `"use client"` directive ensures this component only runs in the browser. + +```tsx +// providers/CometChatCallsProvider.tsx +"use client"; + +import { createContext, useContext, useEffect, useState, ReactNode } from "react"; + +interface User { + uid: string; + name: string; + avatar?: string; +} + +interface CometChatCallsContextType { + isReady: boolean; + user: User | null; + error: string | null; + CometChatCalls: any; +} + +const CometChatCallsContext = createContext({ + isReady: false, + user: null, + error: null, + CometChatCalls: null, +}); + +interface ProviderProps { + children: ReactNode; + uid: string; +} + +export function CometChatCallsProvider({ children, uid }: ProviderProps) { + const [isReady, setIsReady] = useState(false); + const [user, setUser] = useState(null); + const [error, setError] = useState(null); + const [CometChatCalls, setCometChatCalls] = useState(null); + + useEffect(() => { + async function initAndLogin() { + try { + // Dynamic import ensures the SDK only loads on the client + const { CometChatCalls: SDK } = await import("@cometchat/calls-sdk-javascript"); + + // Step 1: Initialize the SDK + const initResult = await SDK.init({ + appId: process.env.NEXT_PUBLIC_COMETCHAT_APP_ID!, + region: process.env.NEXT_PUBLIC_COMETCHAT_REGION!, + }); + + if (!initResult.success) { + throw new Error("SDK initialization failed"); + } + + // Step 2: Check if already logged in + let loggedInUser = SDK.getLoggedInUser(); + + // Step 3: Login if not already logged in + if (!loggedInUser) { + loggedInUser = await SDK.login( + uid, + process.env.NEXT_PUBLIC_COMETCHAT_API_KEY! + ); + } + + setCometChatCalls(SDK); + setUser(loggedInUser); + setIsReady(true); + } catch (err: any) { + console.error("CometChat Calls setup failed:", err); + setError(err.message || "Setup failed"); + } + } + + if (uid) { + initAndLogin(); + } + }, [uid]); + + return ( + + {children} + + ); +} + +export function useCometChatCalls(): CometChatCallsContextType { + return useContext(CometChatCallsContext); +} +``` + +## Step 4: Add Provider to Layout (App Router) + +Wrap your application with the provider. Since the provider is a client component, you can still use it in a server component layout: + +```tsx +// app/layout.tsx +import { CometChatCallsProvider } from "@/providers/CometChatCallsProvider"; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + // In a real app, get this from your authentication system + const currentUserId = "cometchat-uid-1"; + + return ( + + + + {children} + + + + ); +} +``` + +## Step 5: Create the Call Component + +Build a client-side call component that handles joining sessions, media controls, and cleanup. The component uses the SDK instance from the context provider: + +```tsx +// components/CallScreen.tsx +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { useCometChatCalls } from "@/providers/CometChatCallsProvider"; + +interface CallScreenProps { + sessionId: string; + onCallEnd?: () => void; +} + +export default function CallScreen({ sessionId, onCallEnd }: CallScreenProps) { + const { isReady, CometChatCalls } = useCometChatCalls(); + const containerRef = useRef(null); + + // Call state + const [isJoined, setIsJoined] = useState(false); + const [isJoining, setIsJoining] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [isVideoOff, setIsVideoOff] = useState(false); + const [error, setError] = useState(null); + + // Store unsubscribe functions for cleanup + const unsubscribersRef = useRef([]); + + useEffect(() => { + // Don't proceed if SDK isn't ready or container isn't mounted + if (!isReady || !CometChatCalls || !containerRef.current || !sessionId) return; + + async function joinCall() { + setIsJoining(true); + setError(null); + + try { + // Register event listeners before joining + unsubscribersRef.current = [ + CometChatCalls.addEventListener("onSessionJoined", () => { + setIsJoined(true); + setIsJoining(false); + }), + CometChatCalls.addEventListener("onSessionLeft", () => { + setIsJoined(false); + onCallEnd?.(); + }), + CometChatCalls.addEventListener("onAudioMuted", () => setIsMuted(true)), + CometChatCalls.addEventListener("onAudioUnMuted", () => setIsMuted(false)), + CometChatCalls.addEventListener("onVideoPaused", () => setIsVideoOff(true)), + CometChatCalls.addEventListener("onVideoResumed", () => setIsVideoOff(false)), + ]; + + // Generate a call token for this session + const tokenResult = await CometChatCalls.generateToken(sessionId); + + // Join the call session + const joinResult = await CometChatCalls.joinSession( + tokenResult.token, + { + sessionType: "VIDEO", + layout: "TILE", + startAudioMuted: false, + startVideoPaused: false, + }, + containerRef.current + ); + + if (joinResult.error) { + throw new Error(joinResult.error.message); + } + } catch (err: any) { + console.error("Failed to join call:", err); + setError(err.message || "Failed to join call"); + setIsJoining(false); + } + } + + joinCall(); + + // Cleanup when component unmounts + return () => { + unsubscribersRef.current.forEach((unsub) => unsub()); + unsubscribersRef.current = []; + CometChatCalls?.leaveSession(); + }; + }, [isReady, CometChatCalls, sessionId, onCallEnd]); + + // Control handlers + const toggleAudio = () => { + isMuted ? CometChatCalls.unMuteAudio() : CometChatCalls.muteAudio(); + }; + + const toggleVideo = () => { + isVideoOff ? CometChatCalls.resumeVideo() : CometChatCalls.pauseVideo(); + }; + + const leaveCall = () => { + CometChatCalls.leaveSession(); + }; + + // Loading state + if (!isReady) { + return
Initializing...
; + } + + // Error state + if (error) { + return ( +
+

Error: {error}

+ +
+ ); + } + + return ( +
+ {/* Video container - SDK renders the call UI here */} +
+ + {/* Loading overlay */} + {isJoining && ( +
Joining call...
+ )} + + {/* Call controls */} + {isJoined && ( +
+ + + +
+ )} +
+ ); +} +``` + +## Step 6: Create the Call Page + +Create a page that uses the call component. This example shows a simple interface where users can enter a session ID and join a call: + +```tsx +// app/page.tsx +"use client"; + +import { useState } from "react"; +import { useCometChatCalls } from "@/providers/CometChatCallsProvider"; +import CallScreen from "@/components/CallScreen"; + +export default function HomePage() { + const { isReady, user, error } = useCometChatCalls(); + const [sessionId, setSessionId] = useState(""); + const [isInCall, setIsInCall] = useState(false); + + if (error) { + return ( +
+

Error: {error}

+
+ ); + } + + if (!isReady) { + return ( +
+

Loading...

+
+ ); + } + + if (isInCall) { + return ( + setIsInCall(false)} + /> + ); + } + + return ( +
+

CometChat Calls

+

Logged in as: {user?.name || user?.uid}

+ +
+ setSessionId(e.target.value)} + className="w-full p-3 border rounded-lg" + /> + +
+
+ ); +} +``` + +## Dynamic Route for Call Sessions + +For a cleaner URL structure, create a dynamic route that accepts the session ID as a parameter: + +```tsx +// app/call/[sessionId]/page.tsx +"use client"; + +import { useParams, useRouter } from "next/navigation"; +import CallScreen from "@/components/CallScreen"; + +export default function CallPage() { + const params = useParams(); + const router = useRouter(); + const sessionId = params.sessionId as string; + + return ( + router.push("/")} + /> + ); +} +``` + +## Pages Router Setup + +If you're using the Pages Router instead of the App Router, use dynamic imports to prevent server-side rendering of the SDK: + +```tsx +// pages/_app.tsx +import type { AppProps } from "next/app"; +import dynamic from "next/dynamic"; + +const CometChatCallsProvider = dynamic( + () => import("@/providers/CometChatCallsProvider").then(mod => mod.CometChatCallsProvider), + { ssr: false } +); + +export default function App({ Component, pageProps }: AppProps) { + const currentUserId = "cometchat-uid-1"; + + return ( + + + + ); +} +``` + +```tsx +// pages/call/[sessionId].tsx +import dynamic from "next/dynamic"; +import { useRouter } from "next/router"; + +const CallScreen = dynamic(() => import("@/components/CallScreen"), { + ssr: false, + loading: () =>
Loading call...
, +}); + +export default function CallPage() { + const router = useRouter(); + const { sessionId } = router.query; + + if (!sessionId) { + return
Loading...
; + } + + return ( + router.push("/")} + /> + ); +} +``` + +## Custom Hook (Optional) + +For more complex applications, extract call logic into a reusable hook: + +```tsx +// hooks/useCall.ts +"use client"; + +import { useState, useCallback, useRef, useEffect } from "react"; +import { useCometChatCalls } from "@/providers/CometChatCallsProvider"; + +export function useCall() { + const { CometChatCalls, isReady } = useCometChatCalls(); + const [isInCall, setIsInCall] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [isVideoOff, setIsVideoOff] = useState(false); + const [participants, setParticipants] = useState([]); + const unsubscribersRef = useRef([]); + + const joinCall = useCallback(async (sessionId: string, container: HTMLElement, settings = {}) => { + if (!CometChatCalls) return; + + // Setup listeners + unsubscribersRef.current = [ + CometChatCalls.addEventListener("onAudioMuted", () => setIsMuted(true)), + CometChatCalls.addEventListener("onAudioUnMuted", () => setIsMuted(false)), + CometChatCalls.addEventListener("onVideoPaused", () => setIsVideoOff(true)), + CometChatCalls.addEventListener("onVideoResumed", () => setIsVideoOff(false)), + CometChatCalls.addEventListener("onParticipantListChanged", setParticipants), + CometChatCalls.addEventListener("onSessionLeft", () => setIsInCall(false)), + ]; + + const tokenResult = await CometChatCalls.generateToken(sessionId); + await CometChatCalls.joinSession( + tokenResult.token, + { sessionType: "VIDEO", layout: "TILE", ...settings }, + container + ); + setIsInCall(true); + }, [CometChatCalls]); + + const leaveCall = useCallback(() => { + CometChatCalls?.leaveSession(); + unsubscribersRef.current.forEach((unsub) => unsub()); + unsubscribersRef.current = []; + setIsInCall(false); + }, [CometChatCalls]); + + const toggleAudio = useCallback(() => { + isMuted ? CometChatCalls?.unMuteAudio() : CometChatCalls?.muteAudio(); + }, [CometChatCalls, isMuted]); + + const toggleVideo = useCallback(() => { + isVideoOff ? CometChatCalls?.resumeVideo() : CometChatCalls?.pauseVideo(); + }, [CometChatCalls, isVideoOff]); + + // Cleanup on unmount + useEffect(() => { + return () => { + unsubscribersRef.current.forEach((unsub) => unsub()); + }; + }, []); + + return { + isReady, + isInCall, + isMuted, + isVideoOff, + participants, + joinCall, + leaveCall, + toggleAudio, + toggleVideo, + }; +} +``` + +## Related Documentation + +For more detailed information on specific topics covered in this guide, refer to the main documentation: + +- [Setup](/calls/javascript/setup) - Detailed SDK installation and initialization +- [Authentication](/calls/javascript/authentication) - Login methods and user management +- [Session Settings](/calls/javascript/session-settings) - All available call configuration options +- [Join Session](/calls/javascript/join-session) - Session joining and token generation +- [Events](/calls/javascript/events) - Complete list of event listeners +- [Actions](/calls/javascript/actions) - All available call control methods +- [Call Layouts](/calls/javascript/call-layouts) - Layout options and customization +- [Participant Management](/calls/javascript/participant-management) - Managing call participants diff --git a/calls/javascript/overview.mdx b/calls/javascript/overview.mdx index 084f5b29..6b456f17 100644 --- a/calls/javascript/overview.mdx +++ b/calls/javascript/overview.mdx @@ -3,4 +3,153 @@ title: "Calls SDK" sidebarTitle: "Overview" --- -Documentation coming soon for JavaScript Calls SDK. +The CometChat Calls SDK enables real-time voice and video calling capabilities in your web application. Built on top of WebRTC, it provides a complete calling solution with built-in UI components and extensive customization options. + + +**Faster Integration with UI Kits** + +If you're using CometChat UI Kits, voice and video calling is already integrated with ready-to-use components: +- Incoming & outgoing call screens +- Call buttons with one-tap calling +- Call logs with history + +👉 [React UI Kit Call Features](/ui-kit/react/call-features) + +Use this Calls SDK directly only if you need custom call UI or advanced control. + + +## Prerequisites + +Before integrating the Calls SDK, ensure you have: + +1. **CometChat Account**: [Sign up](https://app.cometchat.com/signup) and create an app to get your App ID, Region, and API Key +2. **CometChat Users**: Users must exist in CometChat to use calling features. For testing, create users via the [Dashboard](https://app.cometchat.com) or [REST API](/rest-api/chat-apis/users/create-user). Authentication is handled by the Calls SDK - see [Authentication](/calls/javascript/authentication) +3. **Browser Requirements**: See [Browser Compatibility](#browser-compatibility) below +4. **Permissions**: Camera and microphone permissions for video/audio calls + +## Browser Compatibility + +The Calls SDK requires a modern browser with WebRTC support: + +| Browser | Minimum Version | Notes | +|---------|-----------------|-------| +| Chrome | 72+ | Full support | +| Firefox | 68+ | Full support | +| Safari | 12.1+ | Full support | +| Edge | 79+ | Chromium-based | +| Opera | 60+ | Full support | +| Samsung Internet | 12+ | Full support | + +### Requirements + +- **HTTPS**: Required for camera/microphone access in production. Localhost is exempt during development. +- **WebRTC**: The browser must support WebRTC APIs (`getUserMedia`, `RTCPeerConnection`) +- **JavaScript**: ES6+ support required + +### Mobile Browsers + +| Browser | Support | +|---------|---------| +| Chrome for Android | ✅ Full support | +| Safari for iOS | ✅ iOS 12.1+ | +| Firefox for Android | ✅ Full support | +| Samsung Internet | ✅ Full support | + + +For native mobile apps, consider using the [iOS](/calls/ios/overview) or [Android](/calls/android/overview) SDKs for better performance and native features like CallKit/VoIP. + + +## Framework Integrations + +Get started quickly with framework-specific guides that include complete setup, authentication, and working call implementations: + + + + + Context provider pattern with hooks + + + + Composables and reactive state management + + + + Service-based architecture with RxJS + + + + SSR handling with App Router and Pages Router + + + + Cross-platform with Angular, React, and Vue + + + + +## Call Flow + +```mermaid +sequenceDiagram + participant App + participant CometChatCalls + + App->>CometChatCalls: init() + App->>CometChatCalls: login() + App->>CometChatCalls: generateToken() + App->>CometChatCalls: joinSession() + CometChatCalls-->>App: Session joined + App->>CometChatCalls: Actions (mute, pause, etc.) + CometChatCalls-->>App: Event callbacks + App->>CometChatCalls: leaveSession() +``` + +## Features + + + + + Tile, Sidebar, and Spotlight view modes for different call scenarios + + + + Record call sessions for later playback + + + + Retrieve call history and details + + + + Mute, pin, and manage call participants + + + + Share your screen with other participants + + + + Apply blur or custom image backgrounds + + + + Signal to get attention during calls + + + + Automatic session termination when alone in a call + + + + +## Architecture + +The SDK is organized around these core components: + +| Component | Description | +|-----------|-------------| +| `CometChatCalls` | Main entry point for SDK initialization, authentication, session management, and call actions | +| `CallAppSettings` | Configuration object for SDK initialization (App ID, Region) | +| `CallSettings` | Configuration object for individual call sessions | +| `addEventListener` | Method to register event listeners for session, participant, media, and UI events | + diff --git a/calls/javascript/participant-management.mdx b/calls/javascript/participant-management.mdx new file mode 100644 index 00000000..105430f9 --- /dev/null +++ b/calls/javascript/participant-management.mdx @@ -0,0 +1,170 @@ +--- +title: "Participant Management" +sidebarTitle: "Participant Management" +--- + +Manage call participants including muting, pinning, and monitoring their status during a call. + +## Participant Actions + +### Mute Participant + +Mute a specific participant's audio. This is typically a moderator action. + +```javascript +CometChatCalls.muteParticipant(participantId); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `participantId` | String | The participant's unique identifier | + +### Pause Participant Video + +Pause a specific participant's video. This is typically a moderator action. + +```javascript +CometChatCalls.pauseParticipantVideo(participantId); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `participantId` | String | The participant's unique identifier | + +### Pin Participant + +Pin a participant to keep them prominently displayed regardless of who is speaking. + +```javascript +CometChatCalls.pinParticipant(participantId, type); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `participantId` | String | The participant's unique identifier | +| `type` | String | The participant type | + +### Unpin Participant + +Remove the pin, returning to automatic speaker highlighting. + +```javascript +CometChatCalls.unpinParticipant(); +``` + +## Participant Events + +### Participant Joined + +Fired when a participant joins the call: + +```javascript +CometChatCalls.addEventListener("onParticipantJoined", (participant) => { + console.log(`${participant.name} joined the call`); +}); +``` + +### Participant Left + +Fired when a participant leaves the call: + +```javascript +CometChatCalls.addEventListener("onParticipantLeft", (participant) => { + console.log(`${participant.name} left the call`); +}); +``` + +### Participant List Changed + +Fired when the participant list is updated: + +```javascript +CometChatCalls.addEventListener("onParticipantListChanged", (participants) => { + console.log(`Total participants: ${participants.length}`); + participants.forEach(p => console.log(p.name)); +}); +``` + +### Participant Audio State + +Monitor when participants mute or unmute: + +```javascript +CometChatCalls.addEventListener("onParticipantAudioMuted", (participant) => { + console.log(`${participant.name} muted their audio`); +}); + +CometChatCalls.addEventListener("onParticipantAudioUnmuted", (participant) => { + console.log(`${participant.name} unmuted their audio`); +}); +``` + +### Participant Video State + +Monitor when participants turn their camera on or off: + +```javascript +CometChatCalls.addEventListener("onParticipantVideoPaused", (participant) => { + console.log(`${participant.name} turned off their camera`); +}); + +CometChatCalls.addEventListener("onParticipantVideoResumed", (participant) => { + console.log(`${participant.name} turned on their camera`); +}); +``` + +### Dominant Speaker Changed + +Fired when the active speaker changes: + +```javascript +CometChatCalls.addEventListener("onDominantSpeakerChanged", (participant) => { + console.log(`Active speaker: ${participant.name}`); +}); +``` + +## Participant List UI + +### Show/Hide Participant List Button + +Control visibility of the participant list button: + +```javascript +const callSettings = { + hideParticipantListButton: false, // Show the button + // ... other settings +}; +``` + +### Listen for Participant List Events + +Monitor when the participant list panel is opened or closed: + +```javascript +CometChatCalls.addEventListener("onParticipantListVisible", () => { + console.log("Participant list opened"); +}); + +CometChatCalls.addEventListener("onParticipantListHidden", () => { + console.log("Participant list closed"); +}); +``` + +### Listen for Participant List Button Clicks + +Intercept participant list button clicks: + +```javascript +CometChatCalls.addEventListener("onParticipantListButtonClicked", () => { + console.log("Participant list button clicked"); +}); +``` + +## Participant Object + +| Property | Type | Description | +|----------|------|-------------| +| `uid` | String | Unique identifier (CometChat user ID) | +| `name` | String | Display name | +| `avatar` | String | URL of avatar image | + diff --git a/calls/javascript/permissions-handling.mdx b/calls/javascript/permissions-handling.mdx new file mode 100644 index 00000000..b7b43fb4 --- /dev/null +++ b/calls/javascript/permissions-handling.mdx @@ -0,0 +1,319 @@ +--- +title: "Permissions Handling" +sidebarTitle: "Permissions Handling" +--- + +Handle camera and microphone permissions gracefully to provide a good user experience when joining calls. + +## Permission Requirements + +The Calls SDK requires: +- **Microphone**: Required for audio calls +- **Camera**: Required for video calls (optional for audio-only) + +## Check Permissions Before Joining + +Check if permissions are granted before attempting to join a call: + +```javascript +async function checkMediaPermissions() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true + }); + + // Stop the tracks immediately - we just needed to check permissions + stream.getTracks().forEach(track => track.stop()); + + return { video: true, audio: true }; + } catch (error) { + if (error.name === "NotAllowedError") { + return { video: false, audio: false, denied: true }; + } + if (error.name === "NotFoundError") { + return { video: false, audio: false, notFound: true }; + } + throw error; + } +} +``` + +## Query Permission Status + +Use the Permissions API to check status without prompting: + +```javascript +async function getPermissionStatus() { + const permissions = {}; + + try { + const camera = await navigator.permissions.query({ name: "camera" }); + permissions.camera = camera.state; // "granted", "denied", or "prompt" + + const microphone = await navigator.permissions.query({ name: "microphone" }); + permissions.microphone = microphone.state; + } catch (error) { + // Permissions API not supported + permissions.supported = false; + } + + return permissions; +} + +// Usage +const status = await getPermissionStatus(); +if (status.camera === "denied" || status.microphone === "denied") { + showPermissionDeniedMessage(); +} +``` + +## Handle Permission Denial + +Show helpful messages when permissions are denied: + +```javascript +async function joinCallWithPermissionCheck(sessionId, container) { + const permissions = await checkMediaPermissions(); + + if (permissions.denied) { + showError( + "Camera and microphone access denied. " + + "Please enable permissions in your browser settings and refresh the page." + ); + return; + } + + if (permissions.notFound) { + showError( + "No camera or microphone found. " + + "Please connect a device and try again." + ); + return; + } + + // Permissions granted, proceed with joining + const tokenResult = await CometChatCalls.generateToken(sessionId); + await CometChatCalls.joinSession(tokenResult.token, { sessionType: "VIDEO" }, container); +} +``` + +## Permission Request UI + +Create a pre-call permission check screen: + +```javascript +function PermissionCheck({ onPermissionsGranted, onPermissionsDenied }) { + const [status, setStatus] = useState("checking"); + + useEffect(() => { + checkPermissions(); + }, []); + + async function checkPermissions() { + const result = await checkMediaPermissions(); + + if (result.video && result.audio) { + setStatus("granted"); + onPermissionsGranted(); + } else if (result.denied) { + setStatus("denied"); + onPermissionsDenied(); + } else { + setStatus("prompt"); + } + } + + async function requestPermissions() { + setStatus("requesting"); + const result = await checkMediaPermissions(); + + if (result.video && result.audio) { + setStatus("granted"); + onPermissionsGranted(); + } else { + setStatus("denied"); + onPermissionsDenied(); + } + } + + if (status === "checking") { + return
Checking permissions...
; + } + + if (status === "prompt") { + return ( +
+

We need access to your camera and microphone for the call.

+ +
+ ); + } + + if (status === "denied") { + return ( +
+

Camera and microphone access was denied.

+

To join the call, please:

+
    +
  1. Click the camera icon in your browser's address bar
  2. +
  3. Select "Allow" for camera and microphone
  4. +
  5. Refresh this page
  6. +
+
+ ); + } + + return null; +} +``` + +## Listen for Permission Changes + +Monitor permission changes in real-time: + +```javascript +async function watchPermissions(onChange) { + try { + const camera = await navigator.permissions.query({ name: "camera" }); + const microphone = await navigator.permissions.query({ name: "microphone" }); + + camera.addEventListener("change", () => { + onChange({ camera: camera.state, microphone: microphone.state }); + }); + + microphone.addEventListener("change", () => { + onChange({ camera: camera.state, microphone: microphone.state }); + }); + } catch (error) { + console.log("Permission watching not supported"); + } +} + +// Usage +watchPermissions((status) => { + if (status.camera === "denied" || status.microphone === "denied") { + // Handle permission revocation during call + showWarning("Permissions were revoked. Some features may not work."); + } +}); +``` + +## HTTPS Requirement + +Camera and microphone access requires a secure context: + +```javascript +function isSecureContext() { + // Localhost is considered secure for development + if (window.location.hostname === "localhost" || + window.location.hostname === "127.0.0.1") { + return true; + } + + return window.location.protocol === "https:"; +} + +if (!isSecureContext()) { + showError("Video calls require HTTPS. Please access this page via HTTPS."); +} +``` + +## Browser-Specific Instructions + +Provide browser-specific help for enabling permissions: + +```javascript +function getBrowserPermissionInstructions() { + const ua = navigator.userAgent; + + if (ua.includes("Chrome")) { + return { + browser: "Chrome", + steps: [ + "Click the lock/camera icon in the address bar", + "Set Camera and Microphone to 'Allow'", + "Refresh the page" + ] + }; + } + + if (ua.includes("Firefox")) { + return { + browser: "Firefox", + steps: [ + "Click the permissions icon (camera/lock) in the address bar", + "Remove the block for camera and microphone", + "Refresh the page" + ] + }; + } + + if (ua.includes("Safari")) { + return { + browser: "Safari", + steps: [ + "Go to Safari > Settings > Websites", + "Select Camera and Microphone", + "Set this website to 'Allow'", + "Refresh the page" + ] + }; + } + + return { + browser: "your browser", + steps: [ + "Check your browser settings for camera and microphone permissions", + "Allow access for this website", + "Refresh the page" + ] + }; +} +``` + +## Audio-Only Fallback + +If camera permission is denied but microphone is available, offer audio-only mode: + +```javascript +async function joinWithFallback(sessionId, container) { + let hasVideo = false; + let hasAudio = false; + + // Check audio + try { + const audioStream = await navigator.mediaDevices.getUserMedia({ audio: true }); + audioStream.getTracks().forEach(track => track.stop()); + hasAudio = true; + } catch (e) { + console.log("No audio permission"); + } + + // Check video + try { + const videoStream = await navigator.mediaDevices.getUserMedia({ video: true }); + videoStream.getTracks().forEach(track => track.stop()); + hasVideo = true; + } catch (e) { + console.log("No video permission"); + } + + if (!hasAudio) { + showError("Microphone access is required to join the call."); + return; + } + + const tokenResult = await CometChatCalls.generateToken(sessionId); + + await CometChatCalls.joinSession(tokenResult.token, { + sessionType: hasVideo ? "VIDEO" : "VOICE", + startVideoPaused: !hasVideo, + }, container); + + if (!hasVideo) { + showInfo("Joined in audio-only mode. Enable camera permission for video."); + } +} +``` + diff --git a/calls/javascript/picture-in-picture.mdx b/calls/javascript/picture-in-picture.mdx new file mode 100644 index 00000000..1648a701 --- /dev/null +++ b/calls/javascript/picture-in-picture.mdx @@ -0,0 +1,114 @@ +--- +title: "Picture-in-Picture" +sidebarTitle: "Picture-in-Picture" +--- + +Picture-in-Picture (PiP) allows the call video to continue playing in a floating window while users interact with other content on the page or other browser tabs. + +## Enable Picture-in-Picture + +Enable PiP mode during a call: + +```javascript +CometChatCalls.enablePictureInPictureLayout(); +``` + +## Disable Picture-in-Picture + +Return to the normal call view: + +```javascript +CometChatCalls.disablePictureInPictureLayout(); +``` + +## Browser Support + +Picture-in-Picture support varies by browser: + +| Browser | Support | Notes | +|---------|---------|-------| +| Chrome | ✅ Full support | Chrome 70+ | +| Edge | ✅ Full support | Chromium-based | +| Safari | ✅ Full support | Safari 13.1+ | +| Firefox | ⚠️ Limited | Behind flag in some versions | +| Opera | ✅ Full support | Opera 57+ | + +## Check PiP Support + +Before enabling PiP, check if the browser supports it: + +```javascript +function isPiPSupported() { + return document.pictureInPictureEnabled || false; +} + +if (isPiPSupported()) { + CometChatCalls.enablePictureInPictureLayout(); +} else { + console.log("Picture-in-Picture not supported in this browser"); +} +``` + +## PiP Events + +The SDK doesn't provide specific PiP events, but you can listen to the browser's native PiP events on the video element if needed: + +```javascript +// If you have access to the video element +videoElement.addEventListener("enterpictureinpicture", () => { + console.log("Entered PiP mode"); +}); + +videoElement.addEventListener("leavepictureinpicture", () => { + console.log("Left PiP mode"); +}); +``` + +## User-Initiated PiP + +Browsers typically require PiP to be triggered by a user gesture (click, tap). Wrap the enable call in a button handler: + +```javascript +document.getElementById("pip-btn").addEventListener("click", () => { + CometChatCalls.enablePictureInPictureLayout(); +}); +``` + +## Auto-PiP on Tab Switch + +Some browsers support automatic PiP when switching tabs. This is a browser-level feature and may require user permission. + +```javascript +// Check if auto-PiP is available (Chrome) +if ("documentPictureInPicture" in window) { + // Document PiP API available +} +``` + +## Styling Considerations + +When PiP is active: +- The main call container may appear empty or show a placeholder +- Consider showing a message indicating the call is in PiP mode +- Provide a button to exit PiP and return to the full view + +```javascript +let isPiPActive = false; + +function togglePiP() { + if (isPiPActive) { + CometChatCalls.disablePictureInPictureLayout(); + isPiPActive = false; + } else { + CometChatCalls.enablePictureInPictureLayout(); + isPiPActive = true; + } + updateUI(); +} + +function updateUI() { + const placeholder = document.getElementById("pip-placeholder"); + placeholder.style.display = isPiPActive ? "block" : "none"; +} +``` + diff --git a/calls/javascript/raise-hand.mdx b/calls/javascript/raise-hand.mdx new file mode 100644 index 00000000..b012761d --- /dev/null +++ b/calls/javascript/raise-hand.mdx @@ -0,0 +1,69 @@ +--- +title: "Raise Hand" +sidebarTitle: "Raise Hand" +--- + +The raise hand feature allows participants to signal that they want to speak or get attention during a call without interrupting the current speaker. + +## Raise Hand + +Show a hand-raised indicator to other participants: + +```javascript +CometChatCalls.raiseHand(); +``` + +## Lower Hand + +Remove the hand-raised indicator: + +```javascript +CometChatCalls.lowerHand(); +``` + +## Raise Hand Events + +### Participant Hand Raised + +Fired when a participant raises their hand: + +```javascript +CometChatCalls.addEventListener("onParticipantHandRaised", (participant) => { + console.log(`${participant.name} raised their hand`); + // Show notification or update UI +}); +``` + +### Participant Hand Lowered + +Fired when a participant lowers their hand: + +```javascript +CometChatCalls.addEventListener("onParticipantHandLowered", (participant) => { + console.log(`${participant.name} lowered their hand`); +}); +``` + +## Raise Hand Button + +### Hide Raise Hand Button + +To hide the raise hand button from the control panel: + +```javascript +const callSettings = { + hideRaiseHandButton: true, + // ... other settings +}; +``` + +### Listen for Button Clicks + +Intercept raise hand button clicks for custom behavior: + +```javascript +CometChatCalls.addEventListener("onRaiseHandButtonClicked", () => { + console.log("Raise hand button clicked"); +}); +``` + diff --git a/calls/javascript/react-integration.mdx b/calls/javascript/react-integration.mdx new file mode 100644 index 00000000..132a1435 --- /dev/null +++ b/calls/javascript/react-integration.mdx @@ -0,0 +1,453 @@ +--- +title: "React Integration" +sidebarTitle: "React" +--- + +This guide walks you through integrating the CometChat Calls SDK into a React application. By the end, you'll have a working video call implementation with proper state management and lifecycle handling. + +## Prerequisites + +Before you begin, ensure you have: +- A CometChat account with an app created ([Sign up](https://app.cometchat.com/signup)) +- Your App ID, Region, and API Key from the CometChat Dashboard +- A React project (Create React App, Vite, or similar) +- Node.js 16+ installed + +## Step 1: Install the SDK + +Install the CometChat Calls SDK package: + +```bash +npm install @cometchat/calls-sdk-javascript +``` + +## Step 2: Initialize and Login + +Create a provider component to handle SDK initialization and authentication. This ensures the SDK is ready before any call components render. + +```jsx +// src/providers/CometChatCallsProvider.jsx +import { createContext, useContext, useEffect, useState } from "react"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; + +const CometChatCallsContext = createContext({ + isReady: false, + user: null, + error: null, +}); + +const APP_ID = "YOUR_APP_ID"; // Replace with your App ID +const REGION = "YOUR_REGION"; // Replace with your Region (us, eu, in) +const API_KEY = "YOUR_API_KEY"; // Replace with your API Key + +export function CometChatCallsProvider({ children, uid }) { + const [isReady, setIsReady] = useState(false); + const [user, setUser] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + async function initAndLogin() { + try { + // Step 1: Initialize the SDK + const initResult = await CometChatCalls.init({ + appId: APP_ID, + region: REGION, + }); + + if (!initResult.success) { + throw new Error("SDK initialization failed"); + } + + // Step 2: Check if already logged in + let loggedInUser = CometChatCalls.getLoggedInUser(); + + // Step 3: Login if not already logged in + if (!loggedInUser) { + loggedInUser = await CometChatCalls.login(uid, API_KEY); + } + + setUser(loggedInUser); + setIsReady(true); + } catch (err) { + console.error("CometChat Calls setup failed:", err); + setError(err.message || "Setup failed"); + } + } + + if (uid) { + initAndLogin(); + } + }, [uid]); + + return ( + + {children} + + ); +} + +export function useCometChatCalls() { + return useContext(CometChatCallsContext); +} +``` + +## Step 3: Wrap Your App + +Add the provider to your app's root component: + +```jsx +// src/App.jsx +import { CometChatCallsProvider } from "./providers/CometChatCallsProvider"; +import CallPage from "./pages/CallPage"; + +function App() { + // In a real app, get this from your authentication system + const currentUserId = "cometchat-uid-1"; + + return ( + + + + ); +} + +export default App; +``` + +## Step 4: Create the Call Component + +Build a call component that handles joining, controls, and cleanup: + +```jsx +// src/components/CallScreen.jsx +import { useEffect, useRef, useState } from "react"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; +import { useCometChatCalls } from "../providers/CometChatCallsProvider"; + +export default function CallScreen({ sessionId, onCallEnd }) { + const { isReady } = useCometChatCalls(); + const containerRef = useRef(null); + + // Call state + const [isJoined, setIsJoined] = useState(false); + const [isJoining, setIsJoining] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [isVideoOff, setIsVideoOff] = useState(false); + const [error, setError] = useState(null); + + // Store unsubscribe functions for cleanup + const unsubscribersRef = useRef([]); + + useEffect(() => { + // Don't proceed if SDK isn't ready or container isn't mounted + if (!isReady || !containerRef.current || !sessionId) return; + + async function joinCall() { + setIsJoining(true); + setError(null); + + try { + // Register event listeners before joining + unsubscribersRef.current = [ + CometChatCalls.addEventListener("onSessionJoined", () => { + setIsJoined(true); + setIsJoining(false); + }), + CometChatCalls.addEventListener("onSessionLeft", () => { + setIsJoined(false); + onCallEnd?.(); + }), + CometChatCalls.addEventListener("onAudioMuted", () => setIsMuted(true)), + CometChatCalls.addEventListener("onAudioUnMuted", () => setIsMuted(false)), + CometChatCalls.addEventListener("onVideoPaused", () => setIsVideoOff(true)), + CometChatCalls.addEventListener("onVideoResumed", () => setIsVideoOff(false)), + ]; + + // Generate a call token for this session + const tokenResult = await CometChatCalls.generateToken(sessionId); + + // Join the call session + const joinResult = await CometChatCalls.joinSession( + tokenResult.token, + { + sessionType: "VIDEO", + layout: "TILE", + startAudioMuted: false, + startVideoPaused: false, + }, + containerRef.current + ); + + if (joinResult.error) { + throw new Error(joinResult.error.message); + } + } catch (err) { + console.error("Failed to join call:", err); + setError(err.message || "Failed to join call"); + setIsJoining(false); + } + } + + joinCall(); + + // Cleanup when component unmounts + return () => { + unsubscribersRef.current.forEach((unsub) => unsub()); + unsubscribersRef.current = []; + CometChatCalls.leaveSession(); + }; + }, [isReady, sessionId, onCallEnd]); + + // Control handlers + const toggleAudio = () => { + isMuted ? CometChatCalls.unMuteAudio() : CometChatCalls.muteAudio(); + }; + + const toggleVideo = () => { + isVideoOff ? CometChatCalls.resumeVideo() : CometChatCalls.pauseVideo(); + }; + + const leaveCall = () => { + CometChatCalls.leaveSession(); + }; + + // Loading state + if (!isReady) { + return
Initializing...
; + } + + // Error state + if (error) { + return ( +
+

Error: {error}

+ +
+ ); + } + + return ( +
+ {/* Video container - SDK renders the call UI here */} +
+ + {/* Loading overlay */} + {isJoining && ( +
Joining call...
+ )} + + {/* Call controls */} + {isJoined && ( +
+ + + +
+ )} +
+ ); +} +``` + +## Step 5: Use the Call Component + +Create a page that uses the call component: + +```jsx +// src/pages/CallPage.jsx +import { useState } from "react"; +import { useCometChatCalls } from "../providers/CometChatCallsProvider"; +import CallScreen from "../components/CallScreen"; + +export default function CallPage() { + const { isReady, user, error } = useCometChatCalls(); + const [sessionId, setSessionId] = useState(""); + const [isInCall, setIsInCall] = useState(false); + + if (error) { + return
Error: {error}
; + } + + if (!isReady) { + return
Loading...
; + } + + if (isInCall) { + return ( + setIsInCall(false)} + /> + ); + } + + return ( +
+

CometChat Calls

+

Logged in as: {user?.name || user?.uid}

+ +
+ setSessionId(e.target.value)} + style={{ width: "100%", padding: "12px", marginBottom: "10px" }} + /> + +
+
+ ); +} +``` + +## Custom Hook (Optional) + +For more complex applications, extract call logic into a reusable hook: + +```jsx +// src/hooks/useCall.js +import { useState, useCallback, useRef, useEffect } from "react"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; + +export function useCall() { + const [isInCall, setIsInCall] = useState(false); + const [isMuted, setIsMuted] = useState(false); + const [isVideoOff, setIsVideoOff] = useState(false); + const [participants, setParticipants] = useState([]); + const unsubscribersRef = useRef([]); + + const joinCall = useCallback(async (sessionId, container, settings = {}) => { + // Setup listeners + unsubscribersRef.current = [ + CometChatCalls.addEventListener("onAudioMuted", () => setIsMuted(true)), + CometChatCalls.addEventListener("onAudioUnMuted", () => setIsMuted(false)), + CometChatCalls.addEventListener("onVideoPaused", () => setIsVideoOff(true)), + CometChatCalls.addEventListener("onVideoResumed", () => setIsVideoOff(false)), + CometChatCalls.addEventListener("onParticipantListChanged", setParticipants), + CometChatCalls.addEventListener("onSessionLeft", () => setIsInCall(false)), + ]; + + const tokenResult = await CometChatCalls.generateToken(sessionId); + await CometChatCalls.joinSession( + tokenResult.token, + { sessionType: "VIDEO", layout: "TILE", ...settings }, + container + ); + setIsInCall(true); + }, []); + + const leaveCall = useCallback(() => { + CometChatCalls.leaveSession(); + unsubscribersRef.current.forEach((unsub) => unsub()); + unsubscribersRef.current = []; + setIsInCall(false); + }, []); + + const toggleAudio = useCallback(() => { + isMuted ? CometChatCalls.unMuteAudio() : CometChatCalls.muteAudio(); + }, [isMuted]); + + const toggleVideo = useCallback(() => { + isVideoOff ? CometChatCalls.resumeVideo() : CometChatCalls.pauseVideo(); + }, [isVideoOff]); + + // Cleanup on unmount + useEffect(() => { + return () => { + unsubscribersRef.current.forEach((unsub) => unsub()); + }; + }, []); + + return { + isInCall, + isMuted, + isVideoOff, + participants, + joinCall, + leaveCall, + toggleAudio, + toggleVideo, + }; +} +``` + +## TypeScript Support + +The SDK includes TypeScript definitions. Here's a typed version of the provider: + +```tsx +// src/providers/CometChatCallsProvider.tsx +import { createContext, useContext, useEffect, useState, ReactNode } from "react"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; + +interface User { + uid: string; + name: string; + avatar?: string; +} + +interface CometChatCallsContextType { + isReady: boolean; + user: User | null; + error: string | null; +} + +const CometChatCallsContext = createContext({ + isReady: false, + user: null, + error: null, +}); + +interface ProviderProps { + children: ReactNode; + uid: string; +} + +export function CometChatCallsProvider({ children, uid }: ProviderProps) { + // ... same implementation as above +} + +export function useCometChatCalls(): CometChatCallsContextType { + return useContext(CometChatCallsContext); +} +``` + +## Related Documentation + +For more detailed information on specific topics covered in this guide, refer to the main documentation: + +- [Setup](/calls/javascript/setup) - Detailed SDK installation and initialization +- [Authentication](/calls/javascript/authentication) - Login methods and user management +- [Session Settings](/calls/javascript/session-settings) - All available call configuration options +- [Join Session](/calls/javascript/join-session) - Session joining and token generation +- [Events](/calls/javascript/events) - Complete list of event listeners +- [Actions](/calls/javascript/actions) - All available call control methods +- [Call Layouts](/calls/javascript/call-layouts) - Layout options and customization +- [Participant Management](/calls/javascript/participant-management) - Managing call participants + diff --git a/calls/javascript/recording.mdx b/calls/javascript/recording.mdx new file mode 100644 index 00000000..ada4a3d4 --- /dev/null +++ b/calls/javascript/recording.mdx @@ -0,0 +1,108 @@ +--- +title: "Recording" +sidebarTitle: "Recording" +--- + +Record call sessions for later playback, compliance, or training purposes. Recordings are stored server-side and can be accessed through the CometChat dashboard. + + +Recording must be enabled for your CometChat app. Contact support if you need to enable this feature. + + +## Auto-Start Recording + +Configure recording to start automatically when the session begins: + +```javascript +const callSettings = { + autoStartRecording: true, + // ... other settings +}; + +await CometChatCalls.joinSession(callToken, callSettings, container); +``` + +## Manual Recording Control + +### Start Recording + +Begin recording during an active call: + +```javascript +CometChatCalls.startRecording(); +``` + +### Stop Recording + +Stop the current recording: + +```javascript +CometChatCalls.stopRecording(); +``` + +## Recording Events + +### Recording Started + +Fired when recording begins: + +```javascript +CometChatCalls.addEventListener("onRecordingStarted", () => { + console.log("Recording started"); + // Update UI to show recording indicator +}); +``` + +### Recording Stopped + +Fired when recording ends: + +```javascript +CometChatCalls.addEventListener("onRecordingStopped", () => { + console.log("Recording stopped"); + // Update UI to hide recording indicator +}); +``` + +### Participant Recording Events + +Monitor when other participants start or stop recording: + +```javascript +CometChatCalls.addEventListener("onParticipantStartedRecording", (participant) => { + console.log(`${participant.name} started recording`); +}); + +CometChatCalls.addEventListener("onParticipantStoppedRecording", (participant) => { + console.log(`${participant.name} stopped recording`); +}); +``` + +## Recording Button Visibility + +By default, the recording button is hidden. To show it: + +```javascript +const callSettings = { + hideRecordingButton: false, + // ... other settings +}; +``` + +## Listen for Recording Button Clicks + +Intercept recording button clicks for custom behavior: + +```javascript +CometChatCalls.addEventListener("onRecordingToggleButtonClicked", () => { + console.log("Recording button clicked"); + // Add custom logic like confirmation dialogs +}); +``` + +## Accessing Recordings + +Recordings are available through: +- The CometChat Dashboard under your app's call logs +- The [Call Logs API](/calls/api/list-calls) to retrieve call details including recording URLs + diff --git a/calls/javascript/ringing.mdx b/calls/javascript/ringing.mdx new file mode 100644 index 00000000..ad54370b --- /dev/null +++ b/calls/javascript/ringing.mdx @@ -0,0 +1,282 @@ +--- +title: "Ringing" +sidebarTitle: "Ringing" +--- + +Implement incoming and outgoing call notifications by integrating the Calls SDK with the CometChat Chat SDK for call signaling. + + +The Calls SDK handles the actual call session. For call signaling (ringing, accept, reject), use the CometChat Chat SDK's calling features. + + +## Overview + +Call flow with ringing: + +1. **Initiator** sends a call request via Chat SDK +2. **Receiver** gets notified of incoming call +3. **Receiver** accepts or rejects the call +4. **Both parties** join the call session using Calls SDK + +## Prerequisites + +Install both SDKs: + +```bash +npm install @cometchat/chat-sdk-javascript @cometchat/calls-sdk-javascript +``` + +## Initialize Both SDKs + +```javascript +import { CometChat } from "@cometchat/chat-sdk-javascript"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; + +const appId = "APP_ID"; +const region = "REGION"; + +// Initialize Chat SDK +await CometChat.init(appId, new CometChat.AppSettingsBuilder() + .subscribePresenceForAllUsers() + .setRegion(region) + .build() +); + +// Initialize Calls SDK +await CometChatCalls.init({ appId, region }); + +// Login to both +await CometChat.login(uid, apiKey); +await CometChatCalls.login(uid, apiKey); +``` + +## Initiate a Call + +Start an outgoing call: + +```javascript +async function initiateCall(receiverUid, callType = "video") { + const call = new CometChat.Call( + receiverUid, + callType === "video" ? CometChat.CALL_TYPE.VIDEO : CometChat.CALL_TYPE.AUDIO, + CometChat.RECEIVER_TYPE.USER + ); + + try { + const outgoingCall = await CometChat.initiateCall(call); + console.log("Call initiated:", outgoingCall); + + // Show outgoing call UI + showOutgoingCallScreen(outgoingCall); + + return outgoingCall; + } catch (error) { + console.error("Call initiation failed:", error); + throw error; + } +} +``` + +## Listen for Incoming Calls + +Register a call listener to receive incoming calls: + +```javascript +const callListenerId = "CALL_LISTENER_ID"; + +CometChat.addCallListener( + callListenerId, + new CometChat.CallListener({ + onIncomingCallReceived: (incomingCall) => { + console.log("Incoming call:", incomingCall); + showIncomingCallScreen(incomingCall); + }, + onOutgoingCallAccepted: (acceptedCall) => { + console.log("Call accepted:", acceptedCall); + startCallSession(acceptedCall.getSessionId()); + }, + onOutgoingCallRejected: (rejectedCall) => { + console.log("Call rejected:", rejectedCall); + hideOutgoingCallScreen(); + }, + onIncomingCallCancelled: (cancelledCall) => { + console.log("Call cancelled:", cancelledCall); + hideIncomingCallScreen(); + }, + onCallEndedMessageReceived: (endedCall) => { + console.log("Call ended:", endedCall); + }, + }) +); +``` + +## Accept an Incoming Call + +```javascript +async function acceptCall(incomingCall) { + try { + const acceptedCall = await CometChat.acceptCall(incomingCall.getSessionId()); + console.log("Call accepted:", acceptedCall); + + // Start the call session + startCallSession(acceptedCall.getSessionId()); + } catch (error) { + console.error("Accept call failed:", error); + } +} +``` + +## Reject an Incoming Call + +```javascript +async function rejectCall(incomingCall, reason = CometChat.CALL_STATUS.REJECTED) { + try { + await CometChat.rejectCall(incomingCall.getSessionId(), reason); + console.log("Call rejected"); + hideIncomingCallScreen(); + } catch (error) { + console.error("Reject call failed:", error); + } +} +``` + +## Cancel an Outgoing Call + +```javascript +async function cancelCall(outgoingCall) { + try { + await CometChat.rejectCall(outgoingCall.getSessionId(), CometChat.CALL_STATUS.CANCELLED); + console.log("Call cancelled"); + hideOutgoingCallScreen(); + } catch (error) { + console.error("Cancel call failed:", error); + } +} +``` + +## Start the Call Session + +Once the call is accepted, join the session using the Calls SDK: + +```javascript +async function startCallSession(sessionId) { + const container = document.getElementById("call-container"); + + try { + // Generate token + const tokenResult = await CometChatCalls.generateToken(sessionId); + + // Join session + await CometChatCalls.joinSession( + tokenResult.token, + { + sessionType: "VIDEO", + layout: "TILE", + }, + container + ); + + // Listen for session end + CometChatCalls.addEventListener("onSessionLeft", () => { + endCall(sessionId); + }); + } catch (error) { + console.error("Failed to start call session:", error); + } +} +``` + +## End the Call + +```javascript +async function endCall(sessionId) { + try { + // Leave the Calls SDK session + CometChatCalls.leaveSession(); + + // End the call in Chat SDK + await CometChat.endCall(sessionId); + + console.log("Call ended"); + } catch (error) { + console.error("End call failed:", error); + } +} +``` + +## Incoming Call UI Example + +```javascript +function showIncomingCallScreen(call) { + const caller = call.getCallInitiator(); + + const html = ` +
+
+ ${caller.getName()} +

${caller.getName()}

+

Incoming ${call.getType()} call...

+
+ + +
+
+
+ `; + + document.body.insertAdjacentHTML("beforeend", html); + window.currentCall = call; + + // Play ringtone + playRingtone(); +} + +function hideIncomingCallScreen() { + document.getElementById("incoming-call")?.remove(); + stopRingtone(); +} +``` + +## Outgoing Call UI Example + +```javascript +function showOutgoingCallScreen(call) { + const receiver = call.getCallReceiver(); + + const html = ` +
+
+ ${receiver.getName()} +

${receiver.getName()}

+

Calling...

+ +
+
+ `; + + document.body.insertAdjacentHTML("beforeend", html); + window.currentCall = call; + + // Play ringback tone + playRingbackTone(); +} + +function hideOutgoingCallScreen() { + document.getElementById("outgoing-call")?.remove(); + stopRingbackTone(); +} +``` + +## Cleanup + +Remove the call listener when no longer needed: + +```javascript +CometChat.removeCallListener(callListenerId); +``` + +## Related Documentation + +- [CometChat Chat SDK - Calling](/sdk/javascript/calling-overview) +- [Join Session](/calls/javascript/join-session) + diff --git a/calls/javascript/screen-sharing.mdx b/calls/javascript/screen-sharing.mdx new file mode 100644 index 00000000..d3c70c38 --- /dev/null +++ b/calls/javascript/screen-sharing.mdx @@ -0,0 +1,96 @@ +--- +title: "Screen Sharing" +sidebarTitle: "Screen Sharing" +--- + +Share your screen with other participants during a call. The browser will prompt users to select which screen, window, or browser tab to share. + +## Start Screen Sharing + +Begin sharing your screen: + +```javascript +CometChatCalls.startScreenSharing(); +``` + +The browser will display a dialog allowing the user to choose: +- Entire screen +- Application window +- Browser tab + +## Stop Screen Sharing + +Stop the current screen share: + +```javascript +CometChatCalls.stopScreenSharing(); +``` + +## Screen Sharing Events + +### Local Screen Share Events + +Monitor your own screen sharing state: + +```javascript +CometChatCalls.addEventListener("onScreenShareStarted", () => { + console.log("You started screen sharing"); +}); + +CometChatCalls.addEventListener("onScreenShareStopped", () => { + console.log("You stopped screen sharing"); +}); +``` + +### Participant Screen Share Events + +Monitor when other participants start or stop screen sharing: + +```javascript +CometChatCalls.addEventListener("onParticipantStartedScreenShare", (participant) => { + console.log(`${participant.name} started screen sharing`); +}); + +CometChatCalls.addEventListener("onParticipantStoppedScreenShare", (participant) => { + console.log(`${participant.name} stopped screen sharing`); +}); +``` + +## Screen Sharing Button + +### Hide Screen Sharing Button + +To hide the screen sharing button from the control panel: + +```javascript +const callSettings = { + hideScreenSharingButton: true, + // ... other settings +}; +``` + +### Listen for Button Clicks + +Intercept screen share button clicks for custom behavior: + +```javascript +CometChatCalls.addEventListener("onScreenShareButtonClicked", () => { + console.log("Screen share button clicked"); +}); +``` + +## Browser Support + +Screen sharing requires browser support for the `getDisplayMedia` API: + +| Browser | Support | +|---------|---------| +| Chrome | ✅ Full support | +| Firefox | ✅ Full support | +| Safari | ✅ Safari 13+ | +| Edge | ✅ Full support | + + +Screen sharing requires HTTPS in production. Localhost is exempt during development. + + diff --git a/calls/javascript/session-settings.mdx b/calls/javascript/session-settings.mdx new file mode 100644 index 00000000..d2948b67 --- /dev/null +++ b/calls/javascript/session-settings.mdx @@ -0,0 +1,293 @@ +--- +title: "Session Settings" +sidebarTitle: "Session Settings" +--- + +The session settings object allows you to customize every aspect of your call session before participants join. From controlling the initial audio/video state to customizing the UI layout and hiding specific controls, these settings give you complete control over the call experience. + + +These are pre-session configurations that must be set before joining a call. Once configured, pass the settings object to the `joinSession()` method. Settings cannot be changed after the session has started, though many features can be controlled dynamically during the call using call actions. + + +```javascript +const callSettings = { + sessionType: "VIDEO", + layout: "TILE", + startAudioMuted: false, + startVideoPaused: false, + hideControlPanel: false, + hideLeaveSessionButton: false, + hideToggleAudioButton: false, + hideToggleVideoButton: false, +}; +``` + +## Session Settings + +### Session Type + +**Property:** `sessionType` + +Defines the type of call session. Choose `VIDEO` for video calls with camera enabled, or `VOICE` for audio-only calls. + +```javascript +sessionType: "VIDEO" +``` + +| Value | Description | +|-------|-------------| +| `VIDEO` | Video call with camera enabled | +| `VOICE` | Audio-only call | + +**Default:** `VIDEO` + +### Layout Mode + +**Property:** `layout` + +Sets the initial layout mode for displaying participants. + +```javascript +layout: "TILE" +``` + +| Value | Description | +|-------|-------------| +| `TILE` | Grid layout showing all participants equally | +| `SIDEBAR` | Main speaker with participants in a sidebar | +| `SPOTLIGHT` | Focus on active speaker with others in sidebar | + +**Default:** `TILE` + +### Start Audio Muted + +**Property:** `startAudioMuted` + +Determines whether the microphone is muted when joining the session. + +```javascript +startAudioMuted: true +``` + +**Default:** `false` + +### Start Video Paused + +**Property:** `startVideoPaused` + +Controls whether the camera is turned off when joining the session. + +```javascript +startVideoPaused: true +``` + +**Default:** `false` + +### Auto Start Recording + +**Property:** `autoStartRecording` + +Automatically starts recording the session as soon as it begins. + +```javascript +autoStartRecording: true +``` + +**Default:** `false` + +### Idle Timeout Period Before Prompt + +**Property:** `idleTimeoutPeriodBeforePrompt` + +Time in milliseconds before showing the idle timeout prompt when you're the only participant. + +```javascript +idleTimeoutPeriodBeforePrompt: 60000 // 60 seconds +``` + +**Default:** `60000` (60 seconds) + +### Idle Timeout Period After Prompt + +**Property:** `idleTimeoutPeriodAfterPrompt` + +Time in milliseconds after the prompt before automatically ending the session. + +```javascript +idleTimeoutPeriodAfterPrompt: 120000 // 120 seconds +``` + +**Default:** `120000` (120 seconds) + +## UI Visibility Settings + +### Hide Control Panel + +**Property:** `hideControlPanel` + +Hides the bottom control bar that contains call action buttons. + +```javascript +hideControlPanel: true +``` + +**Default:** `false` + +### Hide Leave Session Button + +**Property:** `hideLeaveSessionButton` + +Hides the button that allows users to leave or end the call. + +```javascript +hideLeaveSessionButton: true +``` + +**Default:** `false` + +### Hide Toggle Audio Button + +**Property:** `hideToggleAudioButton` + +Hides the microphone mute/unmute button from the control panel. + +```javascript +hideToggleAudioButton: true +``` + +**Default:** `false` + +### Hide Toggle Video Button + +**Property:** `hideToggleVideoButton` + +Hides the camera on/off button from the control panel. + +```javascript +hideToggleVideoButton: true +``` + +**Default:** `false` + +### Hide Recording Button + +**Property:** `hideRecordingButton` + +Hides the recording start/stop button from the control panel. + +```javascript +hideRecordingButton: false +``` + +**Default:** `true` + +### Hide Screen Sharing Button + +**Property:** `hideScreenSharingButton` + +Hides the screen sharing button from the control panel. + +```javascript +hideScreenSharingButton: true +``` + +**Default:** `false` + +### Hide Change Layout Button + +**Property:** `hideChangeLayoutButton` + +Hides the button that allows switching between different layout modes. + +```javascript +hideChangeLayoutButton: true +``` + +**Default:** `false` + +### Hide Switch Layout Button + +**Property:** `hideSwitchLayoutButton` + +Hides the layout switch button. + +```javascript +hideSwitchLayoutButton: true +``` + +**Default:** `false` + +### Hide Virtual Background Button + +**Property:** `hideVirtualBackgroundButton` + +Hides the virtual background settings button. + +```javascript +hideVirtualBackgroundButton: true +``` + +**Default:** `false` + +### Hide Network Indicator + +**Property:** `hideNetworkIndicator` + +Hides the network quality indicator. + +```javascript +hideNetworkIndicator: true +``` + +**Default:** `false` + +## Complete Example + +```javascript +const callSettings = { + // Session configuration + sessionType: "VIDEO", + layout: "TILE", + startAudioMuted: false, + startVideoPaused: false, + autoStartRecording: false, + + // Timeout settings + idleTimeoutPeriodBeforePrompt: 60000, + idleTimeoutPeriodAfterPrompt: 120000, + + // UI visibility + hideControlPanel: false, + hideLeaveSessionButton: false, + hideToggleAudioButton: false, + hideToggleVideoButton: false, + hideRecordingButton: true, + hideScreenSharingButton: false, + hideChangeLayoutButton: false, + hideVirtualBackgroundButton: false, + hideNetworkIndicator: false, +}; +``` + + +| Property | Type | Default | Description | +|----------|------|---------|-------------| +| `sessionType` | String | `VIDEO` | Call type: `VIDEO` or `VOICE` | +| `layout` | String | `TILE` | Layout: `TILE`, `SIDEBAR`, or `SPOTLIGHT` | +| `startAudioMuted` | Boolean | `false` | Start with microphone muted | +| `startVideoPaused` | Boolean | `false` | Start with camera off | +| `autoStartRecording` | Boolean | `false` | Auto-start recording | +| `idleTimeoutPeriodBeforePrompt` | Number | `60000` | Idle timeout before prompt (ms) | +| `idleTimeoutPeriodAfterPrompt` | Number | `120000` | Idle timeout after prompt (ms) | +| `hideControlPanel` | Boolean | `false` | Hide control panel | +| `hideLeaveSessionButton` | Boolean | `false` | Hide leave button | +| `hideToggleAudioButton` | Boolean | `false` | Hide audio toggle | +| `hideToggleVideoButton` | Boolean | `false` | Hide video toggle | +| `hideRecordingButton` | Boolean | `true` | Hide recording button | +| `hideScreenSharingButton` | Boolean | `false` | Hide screen share button | +| `hideChangeLayoutButton` | Boolean | `false` | Hide layout change button | +| `hideSwitchLayoutButton` | Boolean | `false` | Hide layout switch button | +| `hideVirtualBackgroundButton` | Boolean | `false` | Hide virtual background button | +| `hideNetworkIndicator` | Boolean | `false` | Hide network indicator | + + diff --git a/calls/javascript/setup.mdx b/calls/javascript/setup.mdx new file mode 100644 index 00000000..3a81c0a4 --- /dev/null +++ b/calls/javascript/setup.mdx @@ -0,0 +1,106 @@ +--- +title: "Setup" +sidebarTitle: "Setup" +--- + +This guide walks you through installing the CometChat Calls SDK and initializing it in your web application. + +## Install the SDK + +Install the CometChat Calls SDK using npm or yarn: + + + +```bash +npm install @cometchat/calls-sdk-javascript +``` + + +```bash +yarn add @cometchat/calls-sdk-javascript +``` + + + +## Import the SDK + +Import the `CometChatCalls` class in your JavaScript or TypeScript file: + +```javascript +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; +``` + +## Initialize CometChat Calls + +The `init()` method initializes the SDK with your app credentials. Call this method once when your application starts. + +### CallAppSettings + +The initialization requires a configuration object with the following properties: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `appId` | String | Yes | Your CometChat App ID | +| `region` | String | Yes | Your app region (`us`, `eu`, or `in`) | + +```javascript +const appId = "APP_ID"; // Replace with your App ID +const region = "REGION"; // Replace with your Region ("us", "eu", or "in") + +const callAppSettings = { + appId: appId, + region: region, +}; + +const result = await CometChatCalls.init(callAppSettings); + +if (result.success) { + console.log("CometChat Calls SDK initialized successfully"); +} else { + console.error("CometChat Calls SDK initialization failed:", result.error); +} +``` + +The `init()` method returns an object with: + +| Property | Type | Description | +|----------|------|-------------| +| `success` | Boolean | `true` if initialization succeeded | +| `error` | Object \| null | Error details if initialization failed | + +## Browser Requirements + +The Calls SDK requires a modern browser with WebRTC support: + +| Browser | Minimum Version | +|---------|-----------------| +| Chrome | 72+ | +| Firefox | 68+ | +| Safari | 12.1+ | +| Edge | 79+ | + + +HTTPS is required for camera and microphone access in production. Localhost is exempt from this requirement during development. + + +## Permissions + +The browser will prompt users for camera and microphone permissions when joining a call. Ensure your application handles permission denials gracefully. + +```javascript +// Check if permissions are granted +async function checkMediaPermissions() { + try { + const stream = await navigator.mediaDevices.getUserMedia({ + video: true, + audio: true + }); + stream.getTracks().forEach(track => track.stop()); + return true; + } catch (error) { + console.error("Media permissions denied:", error); + return false; + } +} +``` + diff --git a/calls/javascript/share-invite.mdx b/calls/javascript/share-invite.mdx new file mode 100644 index 00000000..7865d62a --- /dev/null +++ b/calls/javascript/share-invite.mdx @@ -0,0 +1,93 @@ +--- +title: "Share Invite" +sidebarTitle: "Share Invite" +--- + +Allow participants to share call invitations with others using the share invite feature. + +## Share Invite Button + +### Show Share Invite Button + +By default, the share invite button is hidden. To show it: + +```javascript +const callSettings = { + hideShareInviteButton: false, + // ... other settings +}; +``` + +### Listen for Share Invite Button Clicks + +Handle share invite button clicks to implement your sharing logic: + +```javascript +CometChatCalls.addEventListener("onShareInviteButtonClicked", () => { + console.log("Share invite button clicked"); + // Implement your sharing logic + shareCallInvite(); +}); +``` + +## Implementing Share Functionality + +When the share invite button is clicked, you can implement various sharing methods: + +### Web Share API + +Use the native Web Share API for mobile-friendly sharing: + +```javascript +async function shareCallInvite() { + const shareData = { + title: "Join my call", + text: "Click the link to join my video call", + url: `https://yourapp.com/call/${sessionId}` + }; + + if (navigator.share) { + try { + await navigator.share(shareData); + console.log("Shared successfully"); + } catch (error) { + console.log("Share cancelled or failed"); + } + } else { + // Fallback for browsers that don't support Web Share API + copyToClipboard(shareData.url); + } +} +``` + +### Copy to Clipboard + +Provide a simple copy-to-clipboard option: + +```javascript +function copyToClipboard(text) { + navigator.clipboard.writeText(text).then(() => { + console.log("Link copied to clipboard"); + // Show a toast notification + }).catch(err => { + console.error("Failed to copy:", err); + }); +} +``` + +### Custom Share Dialog + +Create a custom share dialog with multiple options: + +```javascript +function showShareDialog() { + const callLink = `https://yourapp.com/call/${sessionId}`; + + // Show your custom dialog with options like: + // - Copy link + // - Share via email + // - Share via SMS + // - Share to social media +} +``` + diff --git a/calls/javascript/virtual-background.mdx b/calls/javascript/virtual-background.mdx new file mode 100644 index 00000000..bae8b1da --- /dev/null +++ b/calls/javascript/virtual-background.mdx @@ -0,0 +1,81 @@ +--- +title: "Virtual Background" +sidebarTitle: "Virtual Background" +--- + +Apply virtual backgrounds to your video feed during calls. You can blur your background or replace it with a custom image. + +## Background Blur + +Apply a blur effect to your background: + +```javascript +CometChatCalls.setVirtualBackgroundBlurLevel(10); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `blurLevel` | Number | The blur intensity level (higher = more blur) | + +## Custom Background Image + +Set a custom image as your virtual background: + +```javascript +CometChatCalls.setVirtualBackgroundImage("https://example.com/background.jpg"); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `imageUrl` | String | URL of the background image | + + +The image URL must be accessible from the user's browser. Consider hosting images on a CDN for best performance. + + +## Clear Virtual Background + +Remove any applied virtual background: + +```javascript +CometChatCalls.clearVirtualBackground(); +``` + +## Virtual Background Dialog + +### Show Dialog + +Open the built-in virtual background settings dialog: + +```javascript +CometChatCalls.showVirtualBackgroundDialog(); +``` + +### Hide Dialog + +Close the virtual background dialog: + +```javascript +CometChatCalls.hideVirtualBackgroundDialog(); +``` + +## Hide Virtual Background Button + +To hide the virtual background button from the control panel: + +```javascript +const callSettings = { + hideVirtualBackgroundButton: true, + // ... other settings +}; +``` + +## Browser Requirements + +Virtual backgrounds use machine learning for background segmentation. Performance may vary based on: +- Device processing power +- Browser version +- Camera resolution + +For best results, use a modern browser on a device with adequate processing power. + diff --git a/calls/javascript/vue-integration.mdx b/calls/javascript/vue-integration.mdx new file mode 100644 index 00000000..2cd5a425 --- /dev/null +++ b/calls/javascript/vue-integration.mdx @@ -0,0 +1,579 @@ +--- +title: "Vue Integration" +sidebarTitle: "Vue" +--- + +This guide walks you through integrating the CometChat Calls SDK into a Vue.js application. By the end, you'll have a working video call implementation with proper state management and lifecycle handling. + +## Prerequisites + +Before you begin, ensure you have: +- A CometChat account with an app created ([Sign up](https://app.cometchat.com/signup)) +- Your App ID, Region, and API Key from the CometChat Dashboard +- A Vue 3 project (Vite, Vue CLI, or Nuxt) +- Node.js 16+ installed + +## Step 1: Install the SDK + +Install the CometChat Calls SDK package: + +```bash +npm install @cometchat/calls-sdk-javascript +``` + +## Step 2: Create a Composable for SDK Management + +Create a composable that handles initialization, login, and provides reactive state: + +```javascript +// src/composables/useCometChatCalls.js +import { ref, readonly } from "vue"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; + +const APP_ID = "YOUR_APP_ID"; // Replace with your App ID +const REGION = "YOUR_REGION"; // Replace with your Region (us, eu, in) +const API_KEY = "YOUR_API_KEY"; // Replace with your API Key + +// Shared state across all components +const isReady = ref(false); +const user = ref(null); +const error = ref(null); +const isInitializing = ref(false); + +export function useCometChatCalls() { + /** + * Initialize the SDK and login the user. + * Call this once when your app starts or when the user authenticates. + */ + async function initAndLogin(uid) { + if (isInitializing.value || isReady.value) return; + + isInitializing.value = true; + error.value = null; + + try { + // Step 1: Initialize the SDK + const initResult = await CometChatCalls.init({ + appId: APP_ID, + region: REGION, + }); + + if (!initResult.success) { + throw new Error("SDK initialization failed"); + } + + // Step 2: Check if already logged in + let loggedInUser = CometChatCalls.getLoggedInUser(); + + // Step 3: Login if not already logged in + if (!loggedInUser) { + loggedInUser = await CometChatCalls.login(uid, API_KEY); + } + + user.value = loggedInUser; + isReady.value = true; + } catch (err) { + console.error("CometChat Calls setup failed:", err); + error.value = err.message || "Setup failed"; + } finally { + isInitializing.value = false; + } + } + + /** + * Logout the current user and reset state. + */ + async function logout() { + try { + await CometChatCalls.logout(); + user.value = null; + isReady.value = false; + } catch (err) { + console.error("Logout failed:", err); + } + } + + return { + // State (readonly to prevent external mutations) + isReady: readonly(isReady), + user: readonly(user), + error: readonly(error), + isInitializing: readonly(isInitializing), + + // Methods + initAndLogin, + logout, + }; +} +``` + +## Step 3: Initialize in App.vue + +Initialize the SDK when your app mounts: + +```vue + + + + +``` + +## Step 4: Create the Call Component + +Build a call component with proper lifecycle management: + +```vue + + + + + + +``` + +## Step 5: Create the Call Page + +Create a page that manages the call flow: + +```vue + + + + + + +``` + +## Call Composable (Optional) + +For reusable call logic across multiple components: + +```javascript +// src/composables/useCall.js +import { ref, onUnmounted } from "vue"; +import { CometChatCalls } from "@cometchat/calls-sdk-javascript"; + +export function useCall() { + const isInCall = ref(false); + const isMuted = ref(false); + const isVideoOff = ref(false); + const participants = ref([]); + const unsubscribers = ref([]); + + async function joinCall(sessionId, container, settings = {}) { + unsubscribers.value = [ + CometChatCalls.addEventListener("onAudioMuted", () => { isMuted.value = true; }), + CometChatCalls.addEventListener("onAudioUnMuted", () => { isMuted.value = false; }), + CometChatCalls.addEventListener("onVideoPaused", () => { isVideoOff.value = true; }), + CometChatCalls.addEventListener("onVideoResumed", () => { isVideoOff.value = false; }), + CometChatCalls.addEventListener("onParticipantListChanged", (list) => { participants.value = list; }), + CometChatCalls.addEventListener("onSessionLeft", () => { isInCall.value = false; }), + ]; + + const tokenResult = await CometChatCalls.generateToken(sessionId); + await CometChatCalls.joinSession( + tokenResult.token, + { sessionType: "VIDEO", layout: "TILE", ...settings }, + container + ); + isInCall.value = true; + } + + function leaveCall() { + CometChatCalls.leaveSession(); + cleanup(); + } + + function toggleAudio() { + isMuted.value ? CometChatCalls.unMuteAudio() : CometChatCalls.muteAudio(); + } + + function toggleVideo() { + isVideoOff.value ? CometChatCalls.resumeVideo() : CometChatCalls.pauseVideo(); + } + + function cleanup() { + unsubscribers.value.forEach((unsub) => unsub()); + unsubscribers.value = []; + isInCall.value = false; + } + + onUnmounted(() => { + cleanup(); + }); + + return { + isInCall, + isMuted, + isVideoOff, + participants, + joinCall, + leaveCall, + toggleAudio, + toggleVideo, + }; +} +``` + +## Vue 2 Support + +For Vue 2 projects using the Options API, see the [Vue 2 migration guide](https://v3-migration.vuejs.org/) or use the `@vue/composition-api` package to use the Composition API in Vue 2. + +## Related Documentation + +For more detailed information on specific topics covered in this guide, refer to the main documentation: + +- [Setup](/calls/javascript/setup) - Detailed SDK installation and initialization +- [Authentication](/calls/javascript/authentication) - Login methods and user management +- [Session Settings](/calls/javascript/session-settings) - All available call configuration options +- [Join Session](/calls/javascript/join-session) - Session joining and token generation +- [Events](/calls/javascript/events) - Complete list of event listeners +- [Actions](/calls/javascript/actions) - All available call control methods +- [Call Layouts](/calls/javascript/call-layouts) - Layout options and customization +- [Participant Management](/calls/javascript/participant-management) - Managing call participants + diff --git a/calls/react-native/actions.mdx b/calls/react-native/actions.mdx new file mode 100644 index 00000000..dd0a2477 --- /dev/null +++ b/calls/react-native/actions.mdx @@ -0,0 +1,329 @@ +--- +title: "Actions" +sidebarTitle: "Actions" +--- + +Control the active call session programmatically using the static methods on `CometChatCalls`. These methods allow you to manage audio, video, screen sharing, and other call features. + +## Audio Controls + +### Mute Audio + +Mute the local user's microphone: + +```tsx +CometChatCalls.muteAudio(); +``` + +### Unmute Audio + +Unmute the local user's microphone: + +```tsx +CometChatCalls.unMuteAudio(); +``` + +## Video Controls + +### Pause Video + +Stop sending the local user's video: + +```tsx +CometChatCalls.pauseVideo(); +``` + +### Resume Video + +Resume sending the local user's video: + +```tsx +CometChatCalls.resumeVideo(); +``` + +### Switch Camera + +Toggle between front and rear cameras: + +```tsx +CometChatCalls.switchCamera(); +``` + +## Session Control + +### Leave Session + +End the call and leave the session: + +```tsx +CometChatCalls.leaveSession(); +``` + + +The `leaveSession()` method ends the call for the local user. Other participants will remain in the call unless they also leave. + + +## Screen Sharing + +### Start Screen Sharing + +Begin sharing your screen with other participants: + +```tsx +CometChatCalls.startScreenSharing(); +``` + +### Stop Screen Sharing + +Stop sharing your screen: + +```tsx +CometChatCalls.stopScreenSharing(); +``` + + +Screen sharing requires additional platform-specific configuration. See [Screen Sharing](/calls/react-native/screen-sharing) for setup instructions. + + +## Raise Hand + +### Raise Hand + +Signal to other participants that you want attention: + +```tsx +CometChatCalls.raiseHand(); +``` + +### Lower Hand + +Lower your raised hand: + +```tsx +CometChatCalls.lowerHand(); +``` + +## Layout Control + +### Set Layout + +Change the call layout programmatically: + +```tsx +// Available layouts: 'TILE', 'SIDEBAR', 'SPOTLIGHT' +CometChatCalls.setLayout('TILE'); +CometChatCalls.setLayout('SIDEBAR'); +CometChatCalls.setLayout('SPOTLIGHT'); +``` + +| Layout | Description | +|--------|-------------| +| `TILE` | Grid layout showing all participants equally | +| `SIDEBAR` | Main video with participants in a sidebar | +| `SPOTLIGHT` | Focus on one participant with others minimized | + +## Picture-in-Picture + +### Enable Picture-in-Picture + +Enable PiP mode to continue the call in a floating window: + +```tsx +CometChatCalls.enablePictureInPictureLayout(); +``` + +### Disable Picture-in-Picture + +Exit PiP mode and return to full-screen: + +```tsx +CometChatCalls.disablePictureInPictureLayout(); +``` + +## Participant Management + +### Pin Participant + +Pin a participant to the main view: + +```tsx +const participantId = 'participant_pid'; +const type = 'human'; // 'human' or 'screen-share' + +CometChatCalls.pinParticipant(participantId, type); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `participantId` | string | The participant's ID | +| `type` | string | `'human'` for user video, `'screen-share'` for screen share | + +### Unpin Participant + +Remove the pinned participant: + +```tsx +CometChatCalls.unpinParticipant(); +``` + +### Mute Participant + +Mute another participant's audio (requires moderator permissions): + +```tsx +const participantId = 'participant_pid'; +CometChatCalls.muteParticipant(participantId); +``` + +### Pause Participant Video + +Pause another participant's video (requires moderator permissions): + +```tsx +const participantId = 'participant_pid'; +CometChatCalls.pauseParticipantVideo(participantId); +``` + +## Chat Integration + +### Set Chat Button Unread Count + +Update the unread message count on the chat button: + +```tsx +CometChatCalls.setChatButtonUnreadCount(5); +``` + +## Complete Example + +```tsx +import React, { useState } from 'react'; +import { View, TouchableOpacity, Text, StyleSheet } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +function CallControls() { + const [isAudioMuted, setIsAudioMuted] = useState(false); + const [isVideoMuted, setIsVideoMuted] = useState(false); + const [isHandRaised, setIsHandRaised] = useState(false); + const [currentLayout, setCurrentLayout] = useState<'TILE' | 'SIDEBAR' | 'SPOTLIGHT'>('TILE'); + + const toggleAudio = () => { + if (isAudioMuted) { + CometChatCalls.unMuteAudio(); + } else { + CometChatCalls.muteAudio(); + } + setIsAudioMuted(!isAudioMuted); + }; + + const toggleVideo = () => { + if (isVideoMuted) { + CometChatCalls.resumeVideo(); + } else { + CometChatCalls.pauseVideo(); + } + setIsVideoMuted(!isVideoMuted); + }; + + const toggleHand = () => { + if (isHandRaised) { + CometChatCalls.lowerHand(); + } else { + CometChatCalls.raiseHand(); + } + setIsHandRaised(!isHandRaised); + }; + + const cycleLayout = () => { + const layouts: Array<'TILE' | 'SIDEBAR' | 'SPOTLIGHT'> = ['TILE', 'SIDEBAR', 'SPOTLIGHT']; + const currentIndex = layouts.indexOf(currentLayout); + const nextLayout = layouts[(currentIndex + 1) % layouts.length]; + CometChatCalls.setLayout(nextLayout); + setCurrentLayout(nextLayout); + }; + + const handleSwitchCamera = () => { + CometChatCalls.switchCamera(); + }; + + const handleEndCall = () => { + CometChatCalls.leaveSession(); + }; + + return ( + + + + {isAudioMuted ? 'Unmute' : 'Mute'} + + + + + + {isVideoMuted ? 'Show Video' : 'Hide Video'} + + + + + Switch Camera + + + + + {isHandRaised ? 'Lower Hand' : 'Raise Hand'} + + + + + Layout: {currentLayout} + + + + End Call + + + ); +} + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + flexWrap: 'wrap', + justifyContent: 'center', + padding: 16, + gap: 8, + }, + button: { + backgroundColor: '#333', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 8, + }, + endButton: { + backgroundColor: '#ff4444', + }, + buttonText: { + color: '#fff', + fontSize: 14, + fontWeight: '600', + }, +}); + +export default CallControls; +``` + +## Legacy Methods + +These methods are deprecated but still available for backward compatibility: + +| Deprecated Method | Replacement | +|-------------------|-------------| +| `startScreenShare()` | `startScreenSharing()` | +| `stopScreenShare()` | `stopScreenSharing()` | +| `endSession()` | `leaveSession()` | + +## Related Documentation + +- [Events](/calls/react-native/events) - Listen for call events +- [Participant Management](/calls/react-native/participant-management) - Manage participants +- [Call Layouts](/calls/react-native/call-layouts) - Layout options diff --git a/calls/react-native/audio-modes.mdx b/calls/react-native/audio-modes.mdx new file mode 100644 index 00000000..b385f35b --- /dev/null +++ b/calls/react-native/audio-modes.mdx @@ -0,0 +1,293 @@ +--- +title: "Audio Modes" +sidebarTitle: "Audio Modes" +--- + +The CometChat Calls SDK supports multiple audio output modes on mobile devices. Users can switch between speaker, earpiece, Bluetooth, and wired headphones. + +## Available Audio Modes + +| Mode | Constant | Description | +|------|----------|-------------| +| Speaker | `CometChatCalls.AUDIO_MODE.SPEAKER` | Phone speaker (loudspeaker) | +| Earpiece | `CometChatCalls.AUDIO_MODE.EARPIECE` | Phone earpiece (for private calls) | +| Bluetooth | `CometChatCalls.AUDIO_MODE.BLUETOOTH` | Connected Bluetooth device | +| Headphones | `CometChatCalls.AUDIO_MODE.HEADPHONES` | Wired headphones | + +## Set Default Audio Mode + +Configure the default audio mode when creating call settings: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setDefaultAudioMode(CometChatCalls.AUDIO_MODE.SPEAKER) + .build(); +``` + +## Show Audio Mode Button + +Enable the audio mode button in the call UI: + +```tsx +const callSettings = new CometChatCalls.CallSettingsBuilder() + .showAudioModeButton(true) + .build(); +``` + +## Listen for Audio Mode Changes + +Subscribe to audio mode change events: + +```tsx +CometChatCalls.addEventListener('onAudioModeChanged', (mode) => { + console.log('Audio mode changed to:', mode); + // mode: 'SPEAKER' | 'EARPIECE' | 'BLUETOOTH' | 'HEADPHONES' +}); +``` + +## Using OngoingCallListener + +Handle audio mode updates through the call settings listener: + +```tsx +const callListener = new CometChatCalls.OngoingCallListener({ + onAudioModesUpdated: (audioModes) => { + console.log('Available audio modes:', audioModes); + // audioModes is an array of available modes + }, +}); + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setCallEventListener(callListener) + .build(); +``` + +## Audio Mode Object + +When receiving audio mode updates, each mode object contains: + +| Property | Type | Description | +|----------|------|-------------| +| `type` | string | Mode type (`SPEAKER`, `EARPIECE`, `BLUETOOTH`, `HEADPHONES`) | +| `selected` | boolean | Whether this mode is currently active | + +## Complete Example + +```tsx +import React, { useState, useEffect } from 'react'; +import { View, TouchableOpacity, Text, StyleSheet, Modal, FlatList } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +type AudioMode = 'SPEAKER' | 'EARPIECE' | 'BLUETOOTH' | 'HEADPHONES'; + +interface AudioModeOption { + type: AudioMode; + label: string; + icon: string; +} + +const audioModeOptions: AudioModeOption[] = [ + { type: 'SPEAKER', label: 'Speaker', icon: '🔊' }, + { type: 'EARPIECE', label: 'Earpiece', icon: '📱' }, + { type: 'BLUETOOTH', label: 'Bluetooth', icon: '🎧' }, + { type: 'HEADPHONES', label: 'Headphones', icon: '🎵' }, +]; + +function AudioModeSelector() { + const [currentMode, setCurrentMode] = useState('SPEAKER'); + const [availableModes, setAvailableModes] = useState(['SPEAKER', 'EARPIECE']); + const [modalVisible, setModalVisible] = useState(false); + + useEffect(() => { + const unsubscribe = CometChatCalls.addEventListener( + 'onAudioModeChanged', + (mode: AudioMode) => { + setCurrentMode(mode); + } + ); + + return () => unsubscribe(); + }, []); + + const selectMode = (mode: AudioMode) => { + // The SDK handles audio mode switching through the UI + // This is for display purposes + setCurrentMode(mode); + setModalVisible(false); + }; + + const getCurrentModeOption = () => { + return audioModeOptions.find((opt) => opt.type === currentMode) || audioModeOptions[0]; + }; + + const renderModeOption = ({ item }: { item: AudioModeOption }) => { + const isAvailable = availableModes.includes(item.type); + const isSelected = currentMode === item.type; + + return ( + isAvailable && selectMode(item.type)} + disabled={!isAvailable} + > + {item.icon} + + {item.label} + + {isSelected && } + + ); + }; + + return ( + + setModalVisible(true)} + > + {getCurrentModeOption().icon} + {getCurrentModeOption().label} + + + setModalVisible(false)} + > + + + Audio Output + item.type} + renderItem={renderModeOption} + /> + setModalVisible(false)} + > + Close + + + + + + ); +} + +const styles = StyleSheet.create({ + button: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#333', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 8, + gap: 8, + }, + buttonIcon: { + fontSize: 18, + }, + buttonText: { + color: '#fff', + fontSize: 14, + fontWeight: '600', + }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'flex-end', + }, + modalContent: { + backgroundColor: '#1a1a1a', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + padding: 20, + }, + modalTitle: { + color: '#fff', + fontSize: 18, + fontWeight: '600', + marginBottom: 16, + textAlign: 'center', + }, + modeOption: { + flexDirection: 'row', + alignItems: 'center', + padding: 16, + borderRadius: 8, + marginBottom: 8, + backgroundColor: '#333', + }, + selectedOption: { + backgroundColor: '#6851D6', + }, + disabledOption: { + opacity: 0.5, + }, + modeIcon: { + fontSize: 24, + marginRight: 12, + }, + modeLabel: { + flex: 1, + color: '#fff', + fontSize: 16, + }, + selectedLabel: { + fontWeight: '600', + }, + disabledLabel: { + color: '#666', + }, + checkmark: { + color: '#fff', + fontSize: 18, + }, + closeButton: { + marginTop: 16, + padding: 16, + alignItems: 'center', + }, + closeButtonText: { + color: '#6851D6', + fontSize: 16, + fontWeight: '600', + }, +}); + +export default AudioModeSelector; +``` + +## Platform Considerations + +### iOS + +- Audio mode switching is handled automatically by iOS based on connected devices +- Bluetooth devices appear when connected +- Headphones are detected when plugged in + +### Android + +- Requires `MODIFY_AUDIO_SETTINGS` permission +- Bluetooth requires `BLUETOOTH` and `BLUETOOTH_CONNECT` permissions +- Audio routing may vary by device manufacturer + +## Related Documentation + +- [Session Settings](/calls/react-native/session-settings) - Configure default audio mode +- [Events](/calls/react-native/events) - Audio mode events +- [Actions](/calls/react-native/actions) - Audio control methods diff --git a/calls/react-native/authentication.mdx b/calls/react-native/authentication.mdx new file mode 100644 index 00000000..c5f66cf0 --- /dev/null +++ b/calls/react-native/authentication.mdx @@ -0,0 +1,194 @@ +--- +title: "Authentication" +sidebarTitle: "Authentication" +--- + +This guide covers initializing the CometChat Calls SDK and authenticating users in your React Native application. + +## Initialize CometChat Calls + +The `init()` method initializes the SDK with your app credentials. Call this method once when your application starts, typically in your app's entry point or a dedicated initialization module. + +### CallAppSettingsBuilder + +The `CallAppSettingsBuilder` class configures the SDK initialization: + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `appId` | string | Yes | Your CometChat App ID | +| `region` | string | Yes | Your app region (`us`, `eu`, or `in`) | + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +const appId = 'APP_ID'; // Replace with your App ID +const region = 'REGION'; // Replace with your Region ("us", "eu", or "in") + +const callAppSettings = new CometChatCalls.CallAppSettingsBuilder() + .setAppId(appId) + .setRegion(region) + .build(); + +const initResult = await CometChatCalls.init(callAppSettings); + +if (initResult.success) { + console.log('CometChat Calls SDK initialized successfully'); +} else { + console.error('CometChat Calls SDK initialization failed:', initResult.error); +} +``` + +## Login + +After initialization, authenticate the user using one of the login methods. + +### Login with UID and Auth Key + +Use this method during development or when you manage authentication on the client side: + +```tsx +const uid = 'USER_UID'; +const authKey = 'AUTH_KEY'; + +try { + const user = await CometChatCalls.login(uid, authKey); + console.log('Login successful:', user); +} catch (error) { + console.error('Login failed:', error); +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `uid` | string | Yes | The unique identifier of the user | +| `authKey` | string | Yes | Your CometChat Auth Key | + + +Never expose your Auth Key in production apps. Use Auth Tokens generated from your backend server instead. + + +### Login with Auth Token + +For production apps, generate an Auth Token on your backend server and use it to authenticate: + +```tsx +const authToken = 'USER_AUTH_TOKEN'; // Token from your backend + +try { + const user = await CometChatCalls.loginWithAuthToken(authToken); + console.log('Login successful:', user); +} catch (error) { + console.error('Login failed:', error); +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `authToken` | string | Yes | Auth token generated from your backend | + +## Logout + +Log out the current user when they sign out of your app: + +```tsx +try { + const message = await CometChatCalls.logout(); + console.log('Logout successful:', message); +} catch (error) { + console.error('Logout failed:', error); +} +``` + +## Check Login Status + +Verify if a user is currently logged in: + +```tsx +const isLoggedIn = CometChatCalls.isUserLoggedIn(); + +if (isLoggedIn) { + console.log('User is logged in'); +} else { + console.log('No user logged in'); +} +``` + +## Get Logged In User + +Retrieve the currently logged-in user's details: + +```tsx +const user = CometChatCalls.getLoggedInUser(); + +if (user) { + console.log('Current user:', user.uid, user.name); +} else { + console.log('No user logged in'); +} +``` + +## Get User Auth Token + +Retrieve the auth token of the currently logged-in user: + +```tsx +const authToken = CometChatCalls.getUserAuthToken(); + +if (authToken) { + console.log('User auth token:', authToken); +} +``` + +## Login Listeners + +Monitor login state changes by adding a login listener: + +```tsx +const listenerId = 'unique_listener_id'; + +CometChatCalls.addLoginListener(listenerId, { + onLoginSuccess: (user) => { + console.log('User logged in:', user); + }, + onLoginFailure: (error) => { + console.error('Login failed:', error); + }, + onLogoutSuccess: () => { + console.log('User logged out'); + }, + onLogoutFailure: (error) => { + console.error('Logout failed:', error); + }, +}); + +// Remove listener when no longer needed +CometChatCalls.removeLoginListener(listenerId); +``` + +## Error Handling + +The SDK throws structured errors with the following properties: + +| Property | Type | Description | +|----------|------|-------------| +| `errorCode` | string | A unique error code | +| `errorDescription` | string | Human-readable error description | + +Common error codes: + +| Error Code | Description | +|------------|-------------| +| `ERROR_SDK_NOT_INITIALIZED` | SDK not initialized. Call `init()` first | +| `ERROR_LOGIN_IN_PROGRESS` | A login operation is already in progress | +| `ERROR_INVALID_UID` | UID is empty or invalid | +| `ERROR_UID_WITH_SPACE` | UID contains spaces | +| `ERROR_API_KEY_NOT_FOUND` | Auth Key is empty | +| `ERROR_BLANK_AUTHTOKEN` | Auth token is empty | +| `ERROR_AUTHTOKEN_WITH_SPACE` | Auth token contains spaces | +| `ERROR_NO_USER_LOGGED_IN` | No user is currently logged in | + +## Related Documentation + +- [Setup](/calls/react-native/setup) - Install and configure the SDK +- [Session Settings](/calls/react-native/session-settings) - Configure call settings +- [Join Session](/calls/react-native/join-session) - Start your first call diff --git a/calls/react-native/background-handling.mdx b/calls/react-native/background-handling.mdx new file mode 100644 index 00000000..f0b84327 --- /dev/null +++ b/calls/react-native/background-handling.mdx @@ -0,0 +1,292 @@ +--- +title: "Background Handling" +sidebarTitle: "Background Handling" +--- + +Keep calls active when your app goes to the background. This requires platform-specific configuration to maintain audio/video streams and handle system interruptions. + +## iOS Configuration + +### Enable Background Modes + +1. Open your project in Xcode +2. Select your target and go to **Signing & Capabilities** +3. Add **Background Modes** capability +4. Enable: + - **Audio, AirPlay, and Picture in Picture** + - **Voice over IP** (for VoIP push notifications) + +### Configure Audio Session + +The SDK automatically configures the audio session, but you can customize it in your native code: + +```swift +// ios/AppDelegate.swift +import AVFoundation + +func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { + // Configure audio session for calls + do { + try AVAudioSession.sharedInstance().setCategory( + .playAndRecord, + mode: .voiceChat, + options: [.allowBluetooth, .allowBluetoothA2DP, .defaultToSpeaker] + ) + try AVAudioSession.sharedInstance().setActive(true) + } catch { + print("Failed to configure audio session: \(error)") + } + + return true +} +``` + +## Android Configuration + +### Add Permissions + +Add to your `AndroidManifest.xml`: + +```xml + + + +``` + +### Configure Foreground Service + +For Android 10+, calls require a foreground service to continue in the background: + +```xml + +``` + +### Keep Screen Awake + +The SDK automatically manages wake locks during calls. No additional configuration is needed. + +## Handle App State Changes + +Monitor app state to handle background transitions: + +```tsx +import { useEffect, useRef } from 'react'; +import { AppState, AppStateStatus } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +function useBackgroundHandling() { + const appState = useRef(AppState.currentState); + + useEffect(() => { + const subscription = AppState.addEventListener( + 'change', + (nextAppState: AppStateStatus) => { + if ( + appState.current.match(/active/) && + nextAppState === 'background' + ) { + console.log('App going to background'); + // Optionally enable PiP + CometChatCalls.enablePictureInPictureLayout(); + } else if ( + appState.current === 'background' && + nextAppState === 'active' + ) { + console.log('App coming to foreground'); + // Optionally disable PiP + CometChatCalls.disablePictureInPictureLayout(); + } + appState.current = nextAppState; + } + ); + + return () => { + subscription.remove(); + }; + }, []); +} + +export default useBackgroundHandling; +``` + +## Handle Audio Interruptions + +Handle system audio interruptions (phone calls, alarms, etc.): + +```tsx +import { useEffect } from 'react'; +import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; + +function useAudioInterruptions() { + useEffect(() => { + if (Platform.OS === 'ios') { + // iOS handles audio interruptions automatically + // The SDK will pause/resume as needed + return; + } + + // Android: Listen for audio focus changes + // This is typically handled by the SDK automatically + }, []); +} + +export default useAudioInterruptions; +``` + +## Connection Events + +Listen for connection state changes: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +// Connection lost (e.g., network issues) +CometChatCalls.addEventListener('onConnectionLost', () => { + console.log('Connection lost - attempting to reconnect'); +}); + +// Connection restored +CometChatCalls.addEventListener('onConnectionRestored', () => { + console.log('Connection restored'); +}); + +// Connection closed +CometChatCalls.addEventListener('onConnectionClosed', () => { + console.log('Connection closed'); +}); +``` + +## Complete Example + +```tsx +import React, { useEffect, useRef, useCallback } from 'react'; +import { View, StyleSheet, AppState, AppStateStatus } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface CallScreenProps { + callToken: string; + callSettings: any; + onCallEnd: () => void; +} + +function CallScreen({ callToken, callSettings, onCallEnd }: CallScreenProps) { + const appState = useRef(AppState.currentState); + const isInCall = useRef(true); + + // Handle app state changes + useEffect(() => { + const subscription = AppState.addEventListener( + 'change', + (nextAppState: AppStateStatus) => { + if (!isInCall.current) return; + + if ( + appState.current.match(/active/) && + nextAppState === 'background' + ) { + // Going to background - enable PiP for video calls + CometChatCalls.enablePictureInPictureLayout(); + } else if ( + appState.current === 'background' && + nextAppState === 'active' + ) { + // Coming to foreground - disable PiP + CometChatCalls.disablePictureInPictureLayout(); + } + appState.current = nextAppState; + } + ); + + return () => { + subscription.remove(); + }; + }, []); + + // Handle connection events + useEffect(() => { + const unsubscribeLost = CometChatCalls.addEventListener( + 'onConnectionLost', + () => { + console.log('Connection lost'); + // Show reconnecting UI + } + ); + + const unsubscribeRestored = CometChatCalls.addEventListener( + 'onConnectionRestored', + () => { + console.log('Connection restored'); + // Hide reconnecting UI + } + ); + + const unsubscribeClosed = CometChatCalls.addEventListener( + 'onConnectionClosed', + () => { + console.log('Connection closed'); + isInCall.current = false; + onCallEnd(); + } + ); + + return () => { + unsubscribeLost(); + unsubscribeRestored(); + unsubscribeClosed(); + }; + }, [onCallEnd]); + + // Cleanup on unmount + useEffect(() => { + return () => { + isInCall.current = false; + }; + }, []); + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000', + }, +}); + +export default CallScreen; +``` + +## Platform Behavior + +### iOS + +| Scenario | Behavior | +|----------|----------| +| App backgrounded | Audio continues, video pauses | +| Phone call received | Call audio is interrupted | +| Phone call ended | Call audio resumes | +| Screen locked | Audio continues | + +### Android + +| Scenario | Behavior | +|----------|----------| +| App backgrounded | Audio continues with foreground service | +| Phone call received | Call audio may be interrupted | +| Screen off | Audio continues with wake lock | + +## Related Documentation + +- [Picture-in-Picture](/calls/react-native/picture-in-picture) - Continue calls in floating window +- [VoIP Calling](/calls/react-native/voip-calling) - Receive calls when app is closed +- [Events](/calls/react-native/events) - Connection events diff --git a/calls/react-native/call-layouts.mdx b/calls/react-native/call-layouts.mdx new file mode 100644 index 00000000..248f78f7 --- /dev/null +++ b/calls/react-native/call-layouts.mdx @@ -0,0 +1,213 @@ +--- +title: "Call Layouts" +sidebarTitle: "Call Layouts" +--- + +The CometChat Calls SDK provides three layout modes for displaying participants during a call. You can set the initial layout in call settings or change it programmatically during the call. + +## Available Layouts + +### Tile Layout + +The default grid layout that displays all participants equally sized in a grid pattern. + +```tsx +CometChatCalls.setLayout('TILE'); +``` + +### Sidebar Layout + +Displays one main participant with other participants in a sidebar. Useful when focusing on a presenter or active speaker. + +```tsx +CometChatCalls.setLayout('SIDEBAR'); +``` + +### Spotlight Layout + +Focuses on a single participant with minimal UI for others. Ideal for presentations or when one participant needs full attention. + +```tsx +CometChatCalls.setLayout('SPOTLIGHT'); +``` + +## Set Initial Layout + +Configure the initial layout when creating call settings: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setMode(CometChatCalls.CALL_MODE.DEFAULT) // or SPOTLIGHT + .build(); +``` + +| Mode | Description | +|------|-------------| +| `CometChatCalls.CALL_MODE.DEFAULT` | Starts with Tile layout | +| `CometChatCalls.CALL_MODE.SPOTLIGHT` | Starts with Spotlight layout | + +## Change Layout During Call + +Change the layout programmatically at any time during the call: + +```tsx +// Switch to Tile layout +CometChatCalls.setLayout('TILE'); + +// Switch to Sidebar layout +CometChatCalls.setLayout('SIDEBAR'); + +// Switch to Spotlight layout +CometChatCalls.setLayout('SPOTLIGHT'); +``` + +## Listen for Layout Changes + +Subscribe to layout change events: + +```tsx +const unsubscribe = CometChatCalls.addEventListener('onCallLayoutChanged', (layout) => { + console.log('Layout changed to:', layout); + // Update UI state if needed +}); + +// Cleanup +unsubscribe(); +``` + +## Spotlight Mode Options + +When using Spotlight layout, you can configure video tile interactions: + +```tsx +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setMode(CometChatCalls.CALL_MODE.SPOTLIGHT) + .enableVideoTileClick(true) // Allow clicking tiles to spotlight + .enableVideoTileDrag(true) // Allow dragging tiles + .build(); +``` + +| Option | Default | Description | +|--------|---------|-------------| +| `enableVideoTileClick` | `true` | Click a participant tile to spotlight them | +| `enableVideoTileDrag` | `true` | Drag tiles to reposition in Spotlight mode | + +## Pin Participant + +Pin a specific participant to the main view in Sidebar or Spotlight layouts: + +```tsx +// Pin a participant +const participantId = 'participant_pid'; +CometChatCalls.pinParticipant(participantId, 'human'); + +// Pin a screen share +CometChatCalls.pinParticipant(participantId, 'screen-share'); + +// Unpin +CometChatCalls.unpinParticipant(); +``` + +## Automatic Layout Changes + +The SDK automatically switches to Sidebar layout when: +- A participant starts screen sharing +- A participant is pinned + +When screen sharing stops or the participant is unpinned, the layout returns to the previous state if it was programmatically set. + +## Complete Example + +```tsx +import React, { useState, useEffect } from 'react'; +import { View, TouchableOpacity, Text, StyleSheet } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +type Layout = 'TILE' | 'SIDEBAR' | 'SPOTLIGHT'; + +function LayoutControls() { + const [currentLayout, setCurrentLayout] = useState('TILE'); + + useEffect(() => { + const unsubscribe = CometChatCalls.addEventListener( + 'onCallLayoutChanged', + (layout: Layout) => { + setCurrentLayout(layout); + } + ); + + return () => unsubscribe(); + }, []); + + const layouts: Layout[] = ['TILE', 'SIDEBAR', 'SPOTLIGHT']; + + return ( + + Layout: {currentLayout} + + {layouts.map((layout) => ( + CometChatCalls.setLayout(layout)} + > + + {layout} + + + ))} + + + ); +} + +const styles = StyleSheet.create({ + container: { + padding: 16, + }, + label: { + color: '#fff', + fontSize: 14, + marginBottom: 8, + }, + buttons: { + flexDirection: 'row', + gap: 8, + }, + button: { + backgroundColor: '#333', + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 8, + }, + activeButton: { + backgroundColor: '#6851D6', + }, + buttonText: { + color: '#999', + fontSize: 12, + fontWeight: '600', + }, + activeButtonText: { + color: '#fff', + }, +}); + +export default LayoutControls; +``` + +## Related Documentation + +- [Actions](/calls/react-native/actions) - Control the call programmatically +- [Participant Management](/calls/react-native/participant-management) - Pin and manage participants +- [Screen Sharing](/calls/react-native/screen-sharing) - Share your screen diff --git a/calls/react-native/call-logs.mdx b/calls/react-native/call-logs.mdx new file mode 100644 index 00000000..3693f70e --- /dev/null +++ b/calls/react-native/call-logs.mdx @@ -0,0 +1,302 @@ +--- +title: "Call Logs" +sidebarTitle: "Call Logs" +--- + +Call logs provide a history of calls made through your application. You can retrieve call logs using the CometChat Chat SDK's call log APIs. + + +Call logs are managed by the CometChat Chat SDK, not the Calls SDK. Ensure you have the Chat SDK integrated to access call history. + + +## Prerequisites + +To use call logs, you need: +1. CometChat Chat SDK integrated (`@cometchat/chat-sdk-react-native`) +2. User authenticated with the Chat SDK + +## Retrieve Call Logs + +Use the `CallLogRequestBuilder` from the Chat SDK to fetch call logs: + +```tsx +import { CometChat } from '@cometchat/chat-sdk-react-native'; + +async function fetchCallLogs() { + const callLogRequest = new CometChat.CallLogRequestBuilder() + .setLimit(30) + .setCallStatus('all') // 'all', 'initiated', 'ongoing', 'ended', 'cancelled', 'rejected', 'busy', 'unanswered' + .build(); + + try { + const callLogs = await callLogRequest.fetchNext(); + console.log('Call logs:', callLogs); + return callLogs; + } catch (error) { + console.error('Error fetching call logs:', error); + throw error; + } +} +``` + +## CallLogRequestBuilder Options + +| Method | Type | Description | +|--------|------|-------------| +| `setLimit(limit)` | number | Maximum number of logs to fetch (default: 30, max: 100) | +| `setCallStatus(status)` | string | Filter by call status | +| `setCallType(type)` | string | Filter by call type (`audio` or `video`) | +| `setCallCategory(category)` | string | Filter by category (`call` for direct calls) | +| `setAuthToken(token)` | string | Auth token for the request | + +### Call Status Values + +| Status | Description | +|--------|-------------| +| `all` | All call logs | +| `initiated` | Calls that were initiated | +| `ongoing` | Currently active calls | +| `ended` | Completed calls | +| `cancelled` | Calls cancelled by the initiator | +| `rejected` | Calls rejected by the receiver | +| `busy` | Calls where receiver was busy | +| `unanswered` | Calls that weren't answered | + +## Call Log Object + +Each call log contains: + +| Property | Type | Description | +|----------|------|-------------| +| `sessionId` | string | Unique session identifier | +| `initiator` | User | User who initiated the call | +| `receiver` | User/Group | Call recipient | +| `callStatus` | string | Final status of the call | +| `callType` | string | `audio` or `video` | +| `initiatedAt` | number | Timestamp when call was initiated | +| `joinedAt` | number | Timestamp when call was joined | +| `endedAt` | number | Timestamp when call ended | +| `duration` | number | Call duration in seconds | +| `participants` | array | List of participants | +| `recordings` | array | List of recordings (if any) | + +## Filter by User + +Get call logs for a specific user: + +```tsx +const callLogRequest = new CometChat.CallLogRequestBuilder() + .setLimit(30) + .setUid('user_uid') + .build(); +``` + +## Filter by Group + +Get call logs for a specific group: + +```tsx +const callLogRequest = new CometChat.CallLogRequestBuilder() + .setLimit(30) + .setGuid('group_guid') + .build(); +``` + +## Pagination + +Fetch call logs in pages: + +```tsx +import { CometChat } from '@cometchat/chat-sdk-react-native'; + +class CallLogManager { + private callLogRequest: CometChat.CallLogRequest; + + constructor() { + this.callLogRequest = new CometChat.CallLogRequestBuilder() + .setLimit(30) + .build(); + } + + async fetchNextPage() { + try { + const callLogs = await this.callLogRequest.fetchNext(); + return callLogs; + } catch (error) { + console.error('Error fetching call logs:', error); + throw error; + } + } +} + +// Usage +const manager = new CallLogManager(); + +// First page +const page1 = await manager.fetchNextPage(); + +// Next page +const page2 = await manager.fetchNextPage(); +``` + +## Complete Example + +```tsx +import React, { useState, useEffect, useCallback } from 'react'; +import { View, FlatList, Text, StyleSheet, ActivityIndicator } from 'react-native'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; + +interface CallLog { + sessionId: string; + initiator: { uid: string; name: string }; + receiver: { uid: string; name: string }; + callStatus: string; + callType: string; + initiatedAt: number; + duration: number; +} + +function CallLogsScreen() { + const [callLogs, setCallLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [loadingMore, setLoadingMore] = useState(false); + const [callLogRequest, setCallLogRequest] = useState(null); + + useEffect(() => { + const request = new CometChat.CallLogRequestBuilder() + .setLimit(30) + .build(); + setCallLogRequest(request); + fetchCallLogs(request); + }, []); + + const fetchCallLogs = async (request: CometChat.CallLogRequest) => { + try { + const logs = await request.fetchNext(); + setCallLogs(logs); + } catch (error) { + console.error('Error fetching call logs:', error); + } finally { + setLoading(false); + } + }; + + const loadMore = useCallback(async () => { + if (!callLogRequest || loadingMore) return; + + setLoadingMore(true); + try { + const moreLogs = await callLogRequest.fetchNext(); + setCallLogs((prev) => [...prev, ...moreLogs]); + } catch (error) { + console.error('Error loading more:', error); + } finally { + setLoadingMore(false); + } + }, [callLogRequest, loadingMore]); + + const formatDuration = (seconds: number) => { + const mins = Math.floor(seconds / 60); + const secs = seconds % 60; + return `${mins}:${secs.toString().padStart(2, '0')}`; + }; + + const formatDate = (timestamp: number) => { + return new Date(timestamp * 1000).toLocaleString(); + }; + + const renderCallLog = ({ item }: { item: CallLog }) => ( + + + + {item.initiator.name} → {item.receiver.name} + + + {item.callType} • {item.callStatus} + + {formatDate(item.initiatedAt)} + + + {item.duration > 0 ? formatDuration(item.duration) : '-'} + + + ); + + if (loading) { + return ( + + + + ); + } + + return ( + item.sessionId} + renderItem={renderCallLog} + onEndReached={loadMore} + onEndReachedThreshold={0.5} + ListFooterComponent={ + loadingMore ? : null + } + ListEmptyComponent={ + + No call logs + + } + /> + ); +} + +const styles = StyleSheet.create({ + centered: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + callLogItem: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 16, + borderBottomWidth: 1, + borderBottomColor: '#eee', + }, + callInfo: { + flex: 1, + }, + participantName: { + fontSize: 16, + fontWeight: '600', + }, + callDetails: { + fontSize: 14, + color: '#666', + marginTop: 4, + }, + callTime: { + fontSize: 12, + color: '#999', + marginTop: 4, + }, + duration: { + fontSize: 14, + color: '#333', + }, + footer: { + padding: 16, + }, + emptyText: { + fontSize: 16, + color: '#999', + }, +}); + +export default CallLogsScreen; +``` + +## Related Documentation + +- [Recording](/calls/react-native/recording) - Access call recordings +- [Ringing](/calls/react-native/ringing) - Implement call notifications diff --git a/calls/react-native/custom-control-panel.mdx b/calls/react-native/custom-control-panel.mdx new file mode 100644 index 00000000..578b0df8 --- /dev/null +++ b/calls/react-native/custom-control-panel.mdx @@ -0,0 +1,323 @@ +--- +title: "Custom Control Panel" +sidebarTitle: "Custom Control Panel" +--- + +Build a custom control panel to replace or extend the default call controls. Hide the default layout and implement your own UI using the SDK's action methods. + +## Hide Default Layout + +Disable the default control panel to use your own: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .enableDefaultLayout(false) + .build(); +``` + +## Available Actions + +Use these methods to control the call from your custom UI: + +| Action | Method | Description | +|--------|--------|-------------| +| Mute Audio | `CometChatCalls.muteAudio()` | Mute local microphone | +| Unmute Audio | `CometChatCalls.unMuteAudio()` | Unmute local microphone | +| Pause Video | `CometChatCalls.pauseVideo()` | Stop sending video | +| Resume Video | `CometChatCalls.resumeVideo()` | Resume sending video | +| Switch Camera | `CometChatCalls.switchCamera()` | Toggle front/rear camera | +| Leave Session | `CometChatCalls.leaveSession()` | End the call | +| Raise Hand | `CometChatCalls.raiseHand()` | Raise hand | +| Lower Hand | `CometChatCalls.lowerHand()` | Lower hand | +| Set Layout | `CometChatCalls.setLayout(layout)` | Change call layout | +| Start Recording | `CometChatCalls.startRecording()` | Start recording | +| Stop Recording | `CometChatCalls.stopRecording()` | Stop recording | +| Start Screen Share | `CometChatCalls.startScreenSharing()` | Share screen | +| Stop Screen Share | `CometChatCalls.stopScreenSharing()` | Stop sharing | +| Enable PiP | `CometChatCalls.enablePictureInPictureLayout()` | Enter PiP mode | +| Disable PiP | `CometChatCalls.disablePictureInPictureLayout()` | Exit PiP mode | + +## Complete Example + +```tsx +import React, { useState, useEffect } from 'react'; +import { View, TouchableOpacity, Text, StyleSheet, SafeAreaView } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface CustomControlPanelProps { + isAudioOnly?: boolean; +} + +function CustomControlPanel({ isAudioOnly = false }: CustomControlPanelProps) { + const [isAudioMuted, setIsAudioMuted] = useState(false); + const [isVideoMuted, setIsVideoMuted] = useState(false); + const [isHandRaised, setIsHandRaised] = useState(false); + const [isRecording, setIsRecording] = useState(false); + const [currentLayout, setCurrentLayout] = useState<'TILE' | 'SIDEBAR' | 'SPOTLIGHT'>('TILE'); + + useEffect(() => { + // Listen for media events + const unsubscribeAudioMuted = CometChatCalls.addEventListener( + 'onAudioMuted', + () => setIsAudioMuted(true) + ); + const unsubscribeAudioUnmuted = CometChatCalls.addEventListener( + 'onAudioUnMuted', + () => setIsAudioMuted(false) + ); + const unsubscribeVideoPaused = CometChatCalls.addEventListener( + 'onVideoPaused', + () => setIsVideoMuted(true) + ); + const unsubscribeVideoResumed = CometChatCalls.addEventListener( + 'onVideoResumed', + () => setIsVideoMuted(false) + ); + const unsubscribeRecordingStarted = CometChatCalls.addEventListener( + 'onRecordingStarted', + () => setIsRecording(true) + ); + const unsubscribeRecordingStopped = CometChatCalls.addEventListener( + 'onRecordingStopped', + () => setIsRecording(false) + ); + const unsubscribeLayoutChanged = CometChatCalls.addEventListener( + 'onCallLayoutChanged', + (layout: 'TILE' | 'SIDEBAR' | 'SPOTLIGHT') => setCurrentLayout(layout) + ); + + return () => { + unsubscribeAudioMuted(); + unsubscribeAudioUnmuted(); + unsubscribeVideoPaused(); + unsubscribeVideoResumed(); + unsubscribeRecordingStarted(); + unsubscribeRecordingStopped(); + unsubscribeLayoutChanged(); + }; + }, []); + + const toggleAudio = () => { + if (isAudioMuted) { + CometChatCalls.unMuteAudio(); + } else { + CometChatCalls.muteAudio(); + } + }; + + const toggleVideo = () => { + if (isVideoMuted) { + CometChatCalls.resumeVideo(); + } else { + CometChatCalls.pauseVideo(); + } + }; + + const toggleHand = () => { + if (isHandRaised) { + CometChatCalls.lowerHand(); + setIsHandRaised(false); + } else { + CometChatCalls.raiseHand(); + setIsHandRaised(true); + } + }; + + const toggleRecording = () => { + if (isRecording) { + CometChatCalls.stopRecording(); + } else { + CometChatCalls.startRecording(); + } + }; + + const cycleLayout = () => { + const layouts: Array<'TILE' | 'SIDEBAR' | 'SPOTLIGHT'> = ['TILE', 'SIDEBAR', 'SPOTLIGHT']; + const currentIndex = layouts.indexOf(currentLayout); + const nextLayout = layouts[(currentIndex + 1) % layouts.length]; + CometChatCalls.setLayout(nextLayout); + }; + + const handleEndCall = () => { + CometChatCalls.leaveSession(); + }; + + return ( + + + {/* Audio Toggle */} + + {isAudioMuted ? '🔇' : '🎤'} + + {isAudioMuted ? 'Unmute' : 'Mute'} + + + + {/* Video Toggle (only for video calls) */} + {!isAudioOnly && ( + + {isVideoMuted ? '📷' : '🎥'} + + {isVideoMuted ? 'Show' : 'Hide'} + + + )} + + {/* Switch Camera (only for video calls) */} + {!isAudioOnly && ( + CometChatCalls.switchCamera()} + > + 🔄 + Flip + + )} + + {/* Raise Hand */} + + {isHandRaised ? '✋' : '🤚'} + + {isHandRaised ? 'Lower' : 'Raise'} + + + + + + {/* Layout Toggle */} + + 📐 + {currentLayout} + + + {/* Recording */} + + {isRecording ? '⏹️' : '⏺️'} + + {isRecording ? 'Stop' : 'Record'} + + + + {/* End Call */} + + 📞 + End + + + + ); +} + +const styles = StyleSheet.create({ + container: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + backgroundColor: 'rgba(0, 0, 0, 0.8)', + paddingVertical: 16, + paddingHorizontal: 8, + }, + controlRow: { + flexDirection: 'row', + justifyContent: 'center', + marginBottom: 12, + gap: 12, + }, + controlButton: { + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#333', + width: 64, + height: 64, + borderRadius: 32, + }, + activeButton: { + backgroundColor: '#6851D6', + }, + handRaisedButton: { + backgroundColor: '#f59e0b', + }, + recordingButton: { + backgroundColor: '#ef4444', + }, + endCallButton: { + backgroundColor: '#dc2626', + }, + buttonIcon: { + fontSize: 24, + }, + buttonLabel: { + color: '#fff', + fontSize: 10, + marginTop: 4, + }, +}); + +export default CustomControlPanel; +``` + +## Using with Call Component + +Combine the custom control panel with the call component: + +```tsx +import React from 'react'; +import { View, StyleSheet } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; +import CustomControlPanel from './CustomControlPanel'; + +interface CallScreenProps { + callToken: string; + isAudioOnly?: boolean; +} + +function CallScreen({ callToken, isAudioOnly = false }: CallScreenProps) { + const callSettings = new CometChatCalls.CallSettingsBuilder() + .enableDefaultLayout(false) // Hide default controls + .setIsAudioOnlyCall(isAudioOnly) + .build(); + + return ( + + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000', + }, +}); + +export default CallScreen; +``` + +## Related Documentation + +- [Actions](/calls/react-native/actions) - All available call actions +- [Events](/calls/react-native/events) - Listen for call events +- [Session Settings](/calls/react-native/session-settings) - Configure call settings diff --git a/calls/react-native/custom-participant-list.mdx b/calls/react-native/custom-participant-list.mdx new file mode 100644 index 00000000..28c46d27 --- /dev/null +++ b/calls/react-native/custom-participant-list.mdx @@ -0,0 +1,513 @@ +--- +title: "Custom Participant List" +sidebarTitle: "Custom Participant List" +--- + +Build a custom participant list UI to display and manage call participants. Listen for participant events and use the SDK's management methods. + +## Get Participants + +Listen for participant list changes: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +CometChatCalls.addEventListener('onParticipantListChanged', (participants) => { + console.log('Participants:', participants); +}); +``` + +## Participant Object + +Each participant contains: + +| Property | Type | Description | +|----------|------|-------------| +| `pid` | string | Participant ID (unique per session) | +| `uid` | string | User ID | +| `name` | string | Display name | +| `avatar` | string | Avatar URL (optional) | + +## Participant Events + +Subscribe to individual participant events: + +```tsx +// Join/Leave +CometChatCalls.addEventListener('onParticipantJoined', (participant) => { + console.log(`${participant.name} joined`); +}); + +CometChatCalls.addEventListener('onParticipantLeft', (participant) => { + console.log(`${participant.name} left`); +}); + +// Audio state +CometChatCalls.addEventListener('onParticipantAudioMuted', (participant) => { + console.log(`${participant.name} muted`); +}); + +CometChatCalls.addEventListener('onParticipantAudioUnmuted', (participant) => { + console.log(`${participant.name} unmuted`); +}); + +// Video state +CometChatCalls.addEventListener('onParticipantVideoPaused', (participant) => { + console.log(`${participant.name} video paused`); +}); + +CometChatCalls.addEventListener('onParticipantVideoResumed', (participant) => { + console.log(`${participant.name} video resumed`); +}); + +// Hand raised +CometChatCalls.addEventListener('onParticipantHandRaised', (participant) => { + console.log(`${participant.name} raised hand`); +}); + +CometChatCalls.addEventListener('onParticipantHandLowered', (participant) => { + console.log(`${participant.name} lowered hand`); +}); + +// Screen sharing +CometChatCalls.addEventListener('onParticipantStartedScreenShare', (participant) => { + console.log(`${participant.name} started screen share`); +}); + +CometChatCalls.addEventListener('onParticipantStoppedScreenShare', (participant) => { + console.log(`${participant.name} stopped screen share`); +}); + +// Dominant speaker +CometChatCalls.addEventListener('onDominantSpeakerChanged', (participant) => { + console.log(`Dominant speaker: ${participant.name}`); +}); +``` + +## Participant Actions + +Manage participants using these methods: + +```tsx +// Pin participant +CometChatCalls.pinParticipant(participantId, 'human'); + +// Unpin +CometChatCalls.unpinParticipant(); + +// Mute participant (requires moderator) +CometChatCalls.muteParticipant(participantId); + +// Pause participant video (requires moderator) +CometChatCalls.pauseParticipantVideo(participantId); +``` + +## Complete Example + +```tsx +import React, { useState, useEffect } from 'react'; +import { + View, + FlatList, + Text, + TouchableOpacity, + StyleSheet, + Image, + Modal, +} from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface Participant { + pid: string; + uid: string; + name: string; + avatar?: string; +} + +interface ParticipantState extends Participant { + isAudioMuted: boolean; + isVideoMuted: boolean; + isHandRaised: boolean; + isScreenSharing: boolean; + isDominantSpeaker: boolean; +} + +function CustomParticipantList() { + const [participants, setParticipants] = useState>( + new Map() + ); + const [pinnedId, setPinnedId] = useState(null); + const [isVisible, setIsVisible] = useState(false); + const [dominantSpeakerId, setDominantSpeakerId] = useState(null); + + useEffect(() => { + const unsubscribers: Array<() => void> = []; + + // Participant list changes + unsubscribers.push( + CometChatCalls.addEventListener( + 'onParticipantListChanged', + (list: Participant[]) => { + setParticipants((prev) => { + const newMap = new Map(prev); + // Add new participants + list.forEach((p) => { + if (!newMap.has(p.pid)) { + newMap.set(p.pid, { + ...p, + isAudioMuted: false, + isVideoMuted: false, + isHandRaised: false, + isScreenSharing: false, + isDominantSpeaker: false, + }); + } + }); + // Remove participants who left + const currentPids = new Set(list.map((p) => p.pid)); + newMap.forEach((_, pid) => { + if (!currentPids.has(pid)) { + newMap.delete(pid); + } + }); + return newMap; + }); + } + ) + ); + + // Audio state + unsubscribers.push( + CometChatCalls.addEventListener( + 'onParticipantAudioMuted', + (p: Participant) => { + updateParticipant(p.pid, { isAudioMuted: true }); + } + ) + ); + + unsubscribers.push( + CometChatCalls.addEventListener( + 'onParticipantAudioUnmuted', + (p: Participant) => { + updateParticipant(p.pid, { isAudioMuted: false }); + } + ) + ); + + // Video state + unsubscribers.push( + CometChatCalls.addEventListener( + 'onParticipantVideoPaused', + (p: Participant) => { + updateParticipant(p.pid, { isVideoMuted: true }); + } + ) + ); + + unsubscribers.push( + CometChatCalls.addEventListener( + 'onParticipantVideoResumed', + (p: Participant) => { + updateParticipant(p.pid, { isVideoMuted: false }); + } + ) + ); + + // Hand raised + unsubscribers.push( + CometChatCalls.addEventListener( + 'onParticipantHandRaised', + (p: Participant) => { + updateParticipant(p.pid, { isHandRaised: true }); + } + ) + ); + + unsubscribers.push( + CometChatCalls.addEventListener( + 'onParticipantHandLowered', + (p: Participant) => { + updateParticipant(p.pid, { isHandRaised: false }); + } + ) + ); + + // Screen sharing + unsubscribers.push( + CometChatCalls.addEventListener( + 'onParticipantStartedScreenShare', + (p: Participant) => { + updateParticipant(p.pid, { isScreenSharing: true }); + } + ) + ); + + unsubscribers.push( + CometChatCalls.addEventListener( + 'onParticipantStoppedScreenShare', + (p: Participant) => { + updateParticipant(p.pid, { isScreenSharing: false }); + } + ) + ); + + // Dominant speaker + unsubscribers.push( + CometChatCalls.addEventListener( + 'onDominantSpeakerChanged', + (p: Participant) => { + setDominantSpeakerId(p.pid); + } + ) + ); + + return () => { + unsubscribers.forEach((unsub) => unsub()); + }; + }, []); + + const updateParticipant = (pid: string, updates: Partial) => { + setParticipants((prev) => { + const newMap = new Map(prev); + const participant = newMap.get(pid); + if (participant) { + newMap.set(pid, { ...participant, ...updates }); + } + return newMap; + }); + }; + + const handlePin = (participant: ParticipantState) => { + if (pinnedId === participant.pid) { + CometChatCalls.unpinParticipant(); + setPinnedId(null); + } else { + CometChatCalls.pinParticipant(participant.pid, 'human'); + setPinnedId(participant.pid); + } + }; + + const handleMute = (participant: ParticipantState) => { + CometChatCalls.muteParticipant(participant.pid); + }; + + const renderParticipant = ({ item }: { item: ParticipantState }) => ( + + + {item.avatar ? ( + + ) : ( + + + {item.name.charAt(0).toUpperCase()} + + + )} + {dominantSpeakerId === item.pid && ( + + )} + + + + {item.name} + + {item.isAudioMuted && 🔇} + {item.isVideoMuted && 📷} + {item.isHandRaised && } + {item.isScreenSharing && 🖥️} + {pinnedId === item.pid && 📌} + + + + + handlePin(item)} + > + + {pinnedId === item.pid ? 'Unpin' : 'Pin'} + + + handleMute(item)} + > + Mute + + + + ); + + const participantList = Array.from(participants.values()); + + return ( + <> + setIsVisible(true)} + > + + 👥 {participantList.length} + + + + setIsVisible(false)} + > + + + + + Participants ({participantList.length}) + + setIsVisible(false)}> + + + + item.pid} + renderItem={renderParticipant} + ListEmptyComponent={ + No participants + } + /> + + + + + ); +} + +const styles = StyleSheet.create({ + toggleButton: { + position: 'absolute', + top: 60, + right: 16, + backgroundColor: 'rgba(0, 0, 0, 0.6)', + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + }, + toggleButtonText: { + color: '#fff', + fontSize: 16, + }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'flex-end', + }, + modalContent: { + backgroundColor: '#1a1a1a', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + maxHeight: '70%', + }, + modalHeader: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 16, + borderBottomWidth: 1, + borderBottomColor: '#333', + }, + modalTitle: { + color: '#fff', + fontSize: 18, + fontWeight: '600', + }, + closeButton: { + color: '#fff', + fontSize: 20, + padding: 4, + }, + participantItem: { + flexDirection: 'row', + alignItems: 'center', + padding: 12, + borderBottomWidth: 1, + borderBottomColor: '#333', + }, + avatarContainer: { + position: 'relative', + }, + avatar: { + width: 44, + height: 44, + borderRadius: 22, + }, + avatarPlaceholder: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: '#6851D6', + justifyContent: 'center', + alignItems: 'center', + }, + avatarText: { + color: '#fff', + fontSize: 18, + fontWeight: '600', + }, + speakingIndicator: { + position: 'absolute', + bottom: 0, + right: 0, + width: 14, + height: 14, + borderRadius: 7, + backgroundColor: '#22c55e', + borderWidth: 2, + borderColor: '#1a1a1a', + }, + participantInfo: { + flex: 1, + marginLeft: 12, + }, + participantName: { + color: '#fff', + fontSize: 16, + fontWeight: '500', + }, + statusIcons: { + flexDirection: 'row', + marginTop: 4, + gap: 4, + }, + statusIcon: { + fontSize: 14, + }, + actions: { + flexDirection: 'row', + gap: 8, + }, + actionButton: { + backgroundColor: '#333', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 4, + }, + actionText: { + color: '#fff', + fontSize: 12, + }, + emptyText: { + color: '#666', + textAlign: 'center', + padding: 20, + }, +}); + +export default CustomParticipantList; +``` + +## Related Documentation + +- [Participant Management](/calls/react-native/participant-management) - Manage participants +- [Events](/calls/react-native/events) - All participant events +- [Custom Control Panel](/calls/react-native/custom-control-panel) - Build custom controls diff --git a/calls/react-native/events.mdx b/calls/react-native/events.mdx new file mode 100644 index 00000000..c219d4db --- /dev/null +++ b/calls/react-native/events.mdx @@ -0,0 +1,336 @@ +--- +title: "Events" +sidebarTitle: "Events" +--- + +The CometChat Calls SDK provides two ways to listen for call events: the `OngoingCallListener` class for legacy-style callbacks, and the `addEventListener` method for modern event subscriptions. + +## OngoingCallListener + +The `OngoingCallListener` class provides callbacks for call events. Pass it to `CallSettingsBuilder.setCallEventListener()`: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +const callListener = new CometChatCalls.OngoingCallListener({ + onUserJoined: (user) => { + console.log('User joined:', user); + }, + onUserLeft: (user) => { + console.log('User left:', user); + }, + onUserListUpdated: (userList) => { + console.log('User list updated:', userList); + }, + onCallEnded: () => { + console.log('Call ended'); + }, + onCallEndButtonPressed: () => { + console.log('End call button pressed'); + }, + onSessionTimeout: () => { + console.log('Session timed out'); + }, + onError: (error) => { + console.error('Call error:', error); + }, + onAudioModesUpdated: (audioModes) => { + console.log('Audio modes updated:', audioModes); + }, + onRecordingStarted: (data) => { + console.log('Recording started:', data); + }, + onRecordingStopped: (data) => { + console.log('Recording stopped:', data); + }, + onUserMuted: (user) => { + console.log('User muted:', user); + }, + onCallSwitchedToVideo: (data) => { + console.log('Call switched to video:', data); + }, +}); + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setCallEventListener(callListener) + .build(); +``` + +### OngoingCallListener Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `onUserJoined` | User object | A user joined the call | +| `onUserLeft` | User object | A user left the call | +| `onUserListUpdated` | User array | The participant list changed | +| `onCallEnded` | - | The call has ended | +| `onCallEndButtonPressed` | - | The end call button was pressed | +| `onSessionTimeout` | - | The session timed out due to inactivity | +| `onError` | Error object | An error occurred | +| `onAudioModesUpdated` | Audio modes array | Available audio modes changed | +| `onRecordingStarted` | Recording data | Recording has started | +| `onRecordingStopped` | Recording data | Recording has stopped | +| `onUserMuted` | User object | A user was muted | +| `onCallSwitchedToVideo` | Session data | An audio call was switched to video | + +## addEventListener + +For more granular control, use `CometChatCalls.addEventListener()` to subscribe to specific events: + +```tsx +const unsubscribe = CometChatCalls.addEventListener('onParticipantJoined', (participant) => { + console.log('Participant joined:', participant); +}); + +// Later, unsubscribe when no longer needed +unsubscribe(); +``` + +### Session Status Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `onSessionJoined` | - | Successfully joined the session | +| `onSessionLeft` | - | Left the session | +| `onConnectionLost` | - | Connection to the session was lost | +| `onConnectionRestored` | - | Connection was restored | +| `onConnectionClosed` | - | Connection was closed | +| `onSessionTimedOut` | - | Session timed out due to inactivity | + +```tsx +CometChatCalls.addEventListener('onSessionJoined', () => { + console.log('Successfully joined the session'); +}); + +CometChatCalls.addEventListener('onSessionLeft', () => { + console.log('Left the session'); +}); + +CometChatCalls.addEventListener('onConnectionLost', () => { + console.log('Connection lost - attempting to reconnect'); +}); + +CometChatCalls.addEventListener('onConnectionRestored', () => { + console.log('Connection restored'); +}); + +CometChatCalls.addEventListener('onSessionTimedOut', () => { + console.log('Session timed out'); +}); +``` + +### Media Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `onAudioMuted` | - | Local audio was muted | +| `onAudioUnMuted` | - | Local audio was unmuted | +| `onVideoPaused` | - | Local video was paused | +| `onVideoResumed` | - | Local video was resumed | +| `onRecordingStarted` | - | Recording started | +| `onRecordingStopped` | - | Recording stopped | +| `onScreenShareStarted` | - | Screen sharing started | +| `onScreenShareStopped` | - | Screen sharing stopped | +| `onAudioModeChanged` | Audio mode | Audio output mode changed | +| `onCameraFacingChanged` | Camera facing | Camera switched (front/rear) | + +```tsx +CometChatCalls.addEventListener('onAudioMuted', () => { + console.log('Audio muted'); +}); + +CometChatCalls.addEventListener('onAudioUnMuted', () => { + console.log('Audio unmuted'); +}); + +CometChatCalls.addEventListener('onVideoPaused', () => { + console.log('Video paused'); +}); + +CometChatCalls.addEventListener('onVideoResumed', () => { + console.log('Video resumed'); +}); + +CometChatCalls.addEventListener('onAudioModeChanged', (mode) => { + console.log('Audio mode changed to:', mode); +}); + +CometChatCalls.addEventListener('onCameraFacingChanged', (facing) => { + console.log('Camera facing:', facing); // 'FRONT' or 'REAR' +}); +``` + +### Participant Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `onParticipantJoined` | Participant | A participant joined | +| `onParticipantLeft` | Participant | A participant left | +| `onParticipantAudioMuted` | Participant | A participant muted their audio | +| `onParticipantAudioUnmuted` | Participant | A participant unmuted their audio | +| `onParticipantVideoPaused` | Participant | A participant paused their video | +| `onParticipantVideoResumed` | Participant | A participant resumed their video | +| `onParticipantHandRaised` | Participant | A participant raised their hand | +| `onParticipantHandLowered` | Participant | A participant lowered their hand | +| `onParticipantStartedScreenShare` | Participant | A participant started screen sharing | +| `onParticipantStoppedScreenShare` | Participant | A participant stopped screen sharing | +| `onParticipantStartedRecording` | Participant | A participant started recording | +| `onParticipantStoppedRecording` | Participant | A participant stopped recording | +| `onDominantSpeakerChanged` | Participant | The dominant speaker changed | +| `onParticipantListChanged` | Participant array | The participant list changed | + +```tsx +CometChatCalls.addEventListener('onParticipantJoined', (participant) => { + console.log('Participant joined:', participant.name); +}); + +CometChatCalls.addEventListener('onParticipantLeft', (participant) => { + console.log('Participant left:', participant.name); +}); + +CometChatCalls.addEventListener('onParticipantListChanged', (participants) => { + console.log('Total participants:', participants.length); +}); + +CometChatCalls.addEventListener('onParticipantHandRaised', (participant) => { + console.log('Hand raised by:', participant.name); +}); + +CometChatCalls.addEventListener('onDominantSpeakerChanged', (participant) => { + console.log('Dominant speaker:', participant.name); +}); +``` + +### Button Click Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `onLeaveSessionButtonClicked` | - | Leave session button clicked | +| `onRaiseHandButtonClicked` | - | Raise hand button clicked | +| `onShareInviteButtonClicked` | - | Share invite button clicked | +| `onChangeLayoutButtonClicked` | - | Change layout button clicked | +| `onParticipantListButtonClicked` | - | Participant list button clicked | +| `onToggleAudioButtonClicked` | - | Toggle audio button clicked | +| `onToggleVideoButtonClicked` | - | Toggle video button clicked | +| `onRecordingToggleButtonClicked` | - | Recording toggle button clicked | +| `onScreenShareButtonClicked` | - | Screen share button clicked | +| `onChatButtonClicked` | - | Chat button clicked | +| `onSwitchCameraButtonClicked` | - | Switch camera button clicked | + +```tsx +CometChatCalls.addEventListener('onLeaveSessionButtonClicked', () => { + console.log('Leave button clicked'); + // Handle leave confirmation +}); + +CometChatCalls.addEventListener('onChatButtonClicked', () => { + console.log('Chat button clicked'); + // Open chat interface +}); +``` + +### Layout Events + +| Event | Payload | Description | +|-------|---------|-------------| +| `onCallLayoutChanged` | Layout | The call layout changed | +| `onParticipantListVisible` | - | Participant list became visible | +| `onParticipantListHidden` | - | Participant list was hidden | +| `onPictureInPictureLayoutEnabled` | - | PiP mode was enabled | +| `onPictureInPictureLayoutDisabled` | - | PiP mode was disabled | + +```tsx +CometChatCalls.addEventListener('onCallLayoutChanged', (layout) => { + console.log('Layout changed to:', layout); // 'TILE', 'SIDEBAR', or 'SPOTLIGHT' +}); + +CometChatCalls.addEventListener('onPictureInPictureLayoutEnabled', () => { + console.log('PiP enabled'); +}); +``` + +## Participant Object + +The participant object contains: + +| Property | Type | Description | +|----------|------|-------------| +| `pid` | string | Participant ID | +| `uid` | string | User ID | +| `name` | string | Display name | +| `avatar` | string | Avatar URL (optional) | + +## Complete Example + +```tsx +import React, { useEffect, useRef } from 'react'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +function useCallEvents(onCallEnd: () => void) { + const unsubscribersRef = useRef void>>([]); + + useEffect(() => { + // Session events + unsubscribersRef.current.push( + CometChatCalls.addEventListener('onSessionJoined', () => { + console.log('Joined session'); + }) + ); + + unsubscribersRef.current.push( + CometChatCalls.addEventListener('onSessionLeft', () => { + console.log('Left session'); + onCallEnd(); + }) + ); + + unsubscribersRef.current.push( + CometChatCalls.addEventListener('onSessionTimedOut', () => { + console.log('Session timed out'); + onCallEnd(); + }) + ); + + // Participant events + unsubscribersRef.current.push( + CometChatCalls.addEventListener('onParticipantJoined', (participant) => { + console.log(`${participant.name} joined`); + }) + ); + + unsubscribersRef.current.push( + CometChatCalls.addEventListener('onParticipantLeft', (participant) => { + console.log(`${participant.name} left`); + }) + ); + + // Media events + unsubscribersRef.current.push( + CometChatCalls.addEventListener('onAudioModeChanged', (mode) => { + console.log('Audio mode:', mode); + }) + ); + + // Layout events + unsubscribersRef.current.push( + CometChatCalls.addEventListener('onCallLayoutChanged', (layout) => { + console.log('Layout:', layout); + }) + ); + + // Cleanup + return () => { + unsubscribersRef.current.forEach((unsubscribe) => unsubscribe()); + unsubscribersRef.current = []; + }; + }, [onCallEnd]); +} + +export default useCallEvents; +``` + +## Related Documentation + +- [Actions](/calls/react-native/actions) - Control the call programmatically +- [Session Settings](/calls/react-native/session-settings) - Configure event listeners +- [Participant Management](/calls/react-native/participant-management) - Manage participants diff --git a/calls/react-native/idle-timeout.mdx b/calls/react-native/idle-timeout.mdx new file mode 100644 index 00000000..b8b24f83 --- /dev/null +++ b/calls/react-native/idle-timeout.mdx @@ -0,0 +1,152 @@ +--- +title: "Idle Timeout" +sidebarTitle: "Idle Timeout" +--- + +The idle timeout feature automatically ends a call session when a participant is alone for a specified period. This prevents abandoned calls from running indefinitely. + +## Configure Idle Timeout + +Set the idle timeout period when creating call settings: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setIdleTimeoutPeriod(180) // 180 seconds (3 minutes) + .build(); +``` + +| Parameter | Type | Default | Description | +|-----------|------|---------|-------------| +| `idleTimeoutPeriod` | number | `180` | Seconds before auto-ending when alone | + +## How It Works + +1. When all other participants leave the call, the idle timeout timer starts +2. A prompt appears 60 seconds before the timeout, allowing the user to extend the session +3. If the user doesn't respond, the session ends automatically +4. If another participant joins, the timer is cancelled + +## Listen for Timeout Events + +### Session Timeout Event + +Listen for when the session times out: + +```tsx +CometChatCalls.addEventListener('onSessionTimedOut', () => { + console.log('Session timed out due to inactivity'); + // Navigate away from call screen +}); +``` + +### Using OngoingCallListener + +```tsx +const callListener = new CometChatCalls.OngoingCallListener({ + onSessionTimeout: () => { + console.log('Session timed out'); + // Handle timeout + }, + onCallEnded: () => { + console.log('Call ended'); + }, +}); + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setIdleTimeoutPeriod(180) + .setCallEventListener(callListener) + .build(); +``` + +## Disable Idle Timeout + +Set a very high value to effectively disable the timeout: + +```tsx +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setIdleTimeoutPeriod(86400) // 24 hours + .build(); +``` + +## Complete Example + +```tsx +import React, { useEffect, useCallback } from 'react'; +import { Alert } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface CallScreenProps { + sessionId: string; + onCallEnd: () => void; +} + +function CallScreen({ sessionId, onCallEnd }: CallScreenProps) { + const handleSessionTimeout = useCallback(() => { + Alert.alert( + 'Session Ended', + 'The call has ended due to inactivity.', + [ + { + text: 'OK', + onPress: onCallEnd, + }, + ] + ); + }, [onCallEnd]); + + useEffect(() => { + const unsubscribe = CometChatCalls.addEventListener( + 'onSessionTimedOut', + handleSessionTimeout + ); + + return () => unsubscribe(); + }, [handleSessionTimeout]); + + useEffect(() => { + async function initializeCall() { + try { + const { token } = await CometChatCalls.generateToken(sessionId); + + const listener = new CometChatCalls.OngoingCallListener({ + onSessionTimeout: handleSessionTimeout, + onCallEnded: onCallEnd, + }); + + const settings = new CometChatCalls.CallSettingsBuilder() + .setIdleTimeoutPeriod(180) // 3 minutes + .setCallEventListener(listener) + .build(); + + // Render call component with token and settings + } catch (error) { + console.error('Failed to initialize call:', error); + } + } + + initializeCall(); + }, [sessionId, handleSessionTimeout, onCallEnd]); + + // ... render call UI + return null; +} + +export default CallScreen; +``` + +## Timeout Behavior + +| Scenario | Behavior | +|----------|----------| +| User is alone | Timer starts counting down | +| Another participant joins | Timer is cancelled | +| User extends session | Timer resets | +| Timer expires | Session ends, `onSessionTimedOut` fires | + +## Related Documentation + +- [Session Settings](/calls/react-native/session-settings) - Configure call settings +- [Events](/calls/react-native/events) - Handle timeout events +- [Join Session](/calls/react-native/join-session) - Start a call session diff --git a/calls/react-native/in-call-chat.mdx b/calls/react-native/in-call-chat.mdx new file mode 100644 index 00000000..472fb98d --- /dev/null +++ b/calls/react-native/in-call-chat.mdx @@ -0,0 +1,485 @@ +--- +title: "In-Call Chat" +sidebarTitle: "In-Call Chat" +--- + +Enable messaging during calls by integrating the CometChat Chat SDK. Users can send and receive text messages while on a call. + + +In-call chat requires the CometChat Chat SDK (`@cometchat/chat-sdk-react-native`) for messaging functionality. + + +## Prerequisites + +1. CometChat Chat SDK integrated +2. User authenticated with the Chat SDK +3. Active call session + +## Update Chat Button Badge + +Update the unread message count on the chat button: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +// Set unread count +CometChatCalls.setChatButtonUnreadCount(5); + +// Clear unread count +CometChatCalls.setChatButtonUnreadCount(0); +``` + +## Listen for Chat Button Click + +Handle when the user clicks the chat button: + +```tsx +CometChatCalls.addEventListener('onChatButtonClicked', () => { + console.log('Chat button clicked'); + // Open chat interface +}); +``` + +## Send Messages + +Use the Chat SDK to send messages during a call: + +```tsx +import { CometChat } from '@cometchat/chat-sdk-react-native'; + +async function sendMessage(receiverId: string, text: string, receiverType: string) { + const message = new CometChat.TextMessage( + receiverId, + text, + receiverType + ); + + try { + const sentMessage = await CometChat.sendMessage(message); + console.log('Message sent:', sentMessage); + return sentMessage; + } catch (error) { + console.error('Error sending message:', error); + throw error; + } +} + +// Send to user +sendMessage('user_uid', 'Hello!', CometChat.RECEIVER_TYPE.USER); + +// Send to group +sendMessage('group_guid', 'Hello everyone!', CometChat.RECEIVER_TYPE.GROUP); +``` + +## Receive Messages + +Listen for incoming messages: + +```tsx +import { CometChat } from '@cometchat/chat-sdk-react-native'; + +const listenerId = 'in_call_chat_listener'; + +CometChat.addMessageListener( + listenerId, + new CometChat.MessageListener({ + onTextMessageReceived: (message) => { + console.log('Text message received:', message); + // Update UI and badge count + }, + onMediaMessageReceived: (message) => { + console.log('Media message received:', message); + }, + }) +); + +// Remove listener when done +CometChat.removeMessageListener(listenerId); +``` + +## Complete Example + +```tsx +import React, { useState, useEffect, useRef } from 'react'; +import { + View, + FlatList, + TextInput, + TouchableOpacity, + Text, + StyleSheet, + Modal, + KeyboardAvoidingView, + Platform, +} from 'react-native'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface Message { + id: string; + text: string; + sender: { + uid: string; + name: string; + }; + sentAt: number; + isOwn: boolean; +} + +interface InCallChatProps { + receiverId: string; + receiverType: string; +} + +function InCallChat({ receiverId, receiverType }: InCallChatProps) { + const [isVisible, setIsVisible] = useState(false); + const [messages, setMessages] = useState([]); + const [inputText, setInputText] = useState(''); + const [unreadCount, setUnreadCount] = useState(0); + const flatListRef = useRef(null); + const currentUserId = useRef(''); + + useEffect(() => { + // Get current user + const user = CometChat.getLoggedinUser(); + if (user) { + currentUserId.current = user.getUid(); + } + + // Listen for chat button click + const unsubscribeChatButton = CometChatCalls.addEventListener( + 'onChatButtonClicked', + () => { + setIsVisible(true); + setUnreadCount(0); + CometChatCalls.setChatButtonUnreadCount(0); + } + ); + + // Listen for messages + const listenerId = 'in_call_chat'; + CometChat.addMessageListener( + listenerId, + new CometChat.MessageListener({ + onTextMessageReceived: (message: any) => { + const newMessage: Message = { + id: message.getId().toString(), + text: message.getText(), + sender: { + uid: message.getSender().getUid(), + name: message.getSender().getName(), + }, + sentAt: message.getSentAt(), + isOwn: message.getSender().getUid() === currentUserId.current, + }; + + setMessages((prev) => [...prev, newMessage]); + + if (!isVisible) { + setUnreadCount((prev) => { + const newCount = prev + 1; + CometChatCalls.setChatButtonUnreadCount(newCount); + return newCount; + }); + } + }, + }) + ); + + // Fetch previous messages + fetchMessages(); + + return () => { + unsubscribeChatButton(); + CometChat.removeMessageListener(listenerId); + }; + }, []); + + const fetchMessages = async () => { + try { + const messagesRequest = new CometChat.MessagesRequestBuilder() + .setUID(receiverId) + .setLimit(50) + .build(); + + const fetchedMessages = await messagesRequest.fetchPrevious(); + + const formattedMessages: Message[] = fetchedMessages + .filter((msg: any) => msg.getType() === 'text') + .map((msg: any) => ({ + id: msg.getId().toString(), + text: msg.getText(), + sender: { + uid: msg.getSender().getUid(), + name: msg.getSender().getName(), + }, + sentAt: msg.getSentAt(), + isOwn: msg.getSender().getUid() === currentUserId.current, + })); + + setMessages(formattedMessages); + } catch (error) { + console.error('Error fetching messages:', error); + } + }; + + const sendMessage = async () => { + if (!inputText.trim()) return; + + const text = inputText.trim(); + setInputText(''); + + try { + const message = new CometChat.TextMessage( + receiverId, + text, + receiverType + ); + + const sentMessage: any = await CometChat.sendMessage(message); + + const newMessage: Message = { + id: sentMessage.getId().toString(), + text: sentMessage.getText(), + sender: { + uid: sentMessage.getSender().getUid(), + name: sentMessage.getSender().getName(), + }, + sentAt: sentMessage.getSentAt(), + isOwn: true, + }; + + setMessages((prev) => [...prev, newMessage]); + + // Scroll to bottom + setTimeout(() => { + flatListRef.current?.scrollToEnd(); + }, 100); + } catch (error) { + console.error('Error sending message:', error); + } + }; + + const renderMessage = ({ item }: { item: Message }) => ( + + {!item.isOwn && ( + {item.sender.name} + )} + {item.text} + + {new Date(item.sentAt * 1000).toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + })} + + + ); + + return ( + <> + {/* Chat toggle button with badge */} + { + setIsVisible(true); + setUnreadCount(0); + CometChatCalls.setChatButtonUnreadCount(0); + }} + > + 💬 + {unreadCount > 0 && ( + + + {unreadCount > 99 ? '99+' : unreadCount} + + + )} + + + {/* Chat modal */} + setIsVisible(false)} + > + + + + Chat + setIsVisible(false)}> + + + + + item.id} + renderItem={renderMessage} + contentContainerStyle={styles.messagesList} + onContentSizeChange={() => flatListRef.current?.scrollToEnd()} + /> + + + + + Send + + + + + + + ); +} + +const styles = StyleSheet.create({ + chatButton: { + position: 'absolute', + top: 60, + left: 16, + backgroundColor: 'rgba(0, 0, 0, 0.6)', + width: 48, + height: 48, + borderRadius: 24, + justifyContent: 'center', + alignItems: 'center', + }, + chatButtonText: { + fontSize: 24, + }, + badge: { + position: 'absolute', + top: -4, + right: -4, + backgroundColor: '#ef4444', + minWidth: 20, + height: 20, + borderRadius: 10, + justifyContent: 'center', + alignItems: 'center', + paddingHorizontal: 4, + }, + badgeText: { + color: '#fff', + fontSize: 12, + fontWeight: '600', + }, + modalContainer: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'flex-end', + }, + chatContainer: { + backgroundColor: '#1a1a1a', + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + height: '60%', + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + padding: 16, + borderBottomWidth: 1, + borderBottomColor: '#333', + }, + headerTitle: { + color: '#fff', + fontSize: 18, + fontWeight: '600', + }, + closeButton: { + color: '#fff', + fontSize: 20, + padding: 4, + }, + messagesList: { + padding: 16, + }, + messageContainer: { + maxWidth: '80%', + padding: 12, + borderRadius: 16, + marginBottom: 8, + }, + ownMessage: { + alignSelf: 'flex-end', + backgroundColor: '#6851D6', + }, + otherMessage: { + alignSelf: 'flex-start', + backgroundColor: '#333', + }, + senderName: { + color: '#999', + fontSize: 12, + marginBottom: 4, + }, + messageText: { + color: '#fff', + fontSize: 16, + }, + messageTime: { + color: 'rgba(255, 255, 255, 0.6)', + fontSize: 10, + marginTop: 4, + alignSelf: 'flex-end', + }, + inputContainer: { + flexDirection: 'row', + padding: 12, + borderTopWidth: 1, + borderTopColor: '#333', + alignItems: 'flex-end', + }, + input: { + flex: 1, + backgroundColor: '#333', + borderRadius: 20, + paddingHorizontal: 16, + paddingVertical: 10, + color: '#fff', + maxHeight: 100, + }, + sendButton: { + marginLeft: 8, + backgroundColor: '#6851D6', + paddingHorizontal: 16, + paddingVertical: 10, + borderRadius: 20, + }, + sendButtonText: { + color: '#fff', + fontWeight: '600', + }, +}); + +export default InCallChat; +``` + +## Related Documentation + +- [Events](/calls/react-native/events) - Chat button events +- [Custom Control Panel](/calls/react-native/custom-control-panel) - Build custom controls +- [Ringing](/calls/react-native/ringing) - Call signaling with Chat SDK diff --git a/calls/react-native/join-session.mdx b/calls/react-native/join-session.mdx new file mode 100644 index 00000000..c5afbaae --- /dev/null +++ b/calls/react-native/join-session.mdx @@ -0,0 +1,349 @@ +--- +title: "Join Session" +sidebarTitle: "Join Session" +--- + +This guide covers generating call tokens and joining call sessions using the CometChat Calls SDK. + +## Generate Token + +Before joining a call, you need to generate a call token. The token authenticates the user for the specific call session. + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +const sessionId = 'UNIQUE_SESSION_ID'; + +try { + const { token } = await CometChatCalls.generateToken(sessionId); + console.log('Call token generated:', token); +} catch (error) { + console.error('Token generation failed:', error); +} +``` + +| Parameter | Type | Required | Description | +|-----------|------|----------|-------------| +| `sessionId` | string | Yes | Unique identifier for the call session | +| `authToken` | string | No | User's auth token (uses logged-in user's token if not provided) | + + +The `sessionId` should be unique for each call. You can use a UUID, a combination of user IDs, or any unique string that identifies the call session. + + +## Join Session with Component + +The `CometChatCalls.Component` renders the call UI. Pass the generated token and call settings to join the session. + +```tsx +import React, { useState, useEffect } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface CallScreenProps { + sessionId: string; + isAudioOnly?: boolean; +} + +function CallScreen({ sessionId, isAudioOnly = false }: CallScreenProps) { + const [callToken, setCallToken] = useState(null); + const [callSettings, setCallSettings] = useState(null); + + useEffect(() => { + async function initializeCall() { + try { + // Generate call token + const { token } = await CometChatCalls.generateToken(sessionId); + setCallToken(token); + + // Create call settings + const listener = new CometChatCalls.OngoingCallListener({ + onUserJoined: (user) => console.log('User joined:', user.name), + onUserLeft: (user) => console.log('User left:', user.name), + onCallEnded: () => console.log('Call ended'), + onError: (error) => console.error('Error:', error), + }); + + const settings = new CometChatCalls.CallSettingsBuilder() + .enableDefaultLayout(true) + .setIsAudioOnlyCall(isAudioOnly) + .setMode(CometChatCalls.CALL_MODE.DEFAULT) + .showEndCallButton(true) + .showMuteAudioButton(true) + .showPauseVideoButton(!isAudioOnly) + .setCallEventListener(listener) + .build(); + + setCallSettings(settings); + } catch (error) { + console.error('Failed to initialize call:', error); + } + } + + initializeCall(); + }, [sessionId, isAudioOnly]); + + if (!callToken || !callSettings) { + return null; // Or show a loading indicator + } + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, +}); + +export default CallScreen; +``` + +## Component Props + +| Prop | Type | Required | Description | +|------|------|----------|-------------| +| `callToken` | string | Yes | Token generated from `generateToken()` | +| `callSettings` | CallSettings | No | Configuration object from `CallSettingsBuilder` | + +## Session ID Strategies + +Choose a session ID strategy based on your use case: + +### 1:1 Calls + +For direct calls between two users, create a deterministic session ID: + +```tsx +function getDirectCallSessionId(userId1: string, userId2: string): string { + // Sort IDs to ensure same session ID regardless of who initiates + const sortedIds = [userId1, userId2].sort(); + return `direct_${sortedIds[0]}_${sortedIds[1]}`; +} + +const sessionId = getDirectCallSessionId('alice', 'bob'); +// Result: "direct_alice_bob" +``` + +### Group Calls + +For group calls, use the group ID or a unique identifier: + +```tsx +function getGroupCallSessionId(groupId: string): string { + return `group_${groupId}`; +} + +const sessionId = getGroupCallSessionId('team-standup'); +// Result: "group_team-standup" +``` + +### Unique Sessions + +For one-time calls, generate a unique ID: + +```tsx +function generateUniqueSessionId(): string { + return `call_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; +} + +const sessionId = generateUniqueSessionId(); +// Result: "call_1704067200000_abc123xyz" +``` + +## Complete Example + +```tsx +import React, { useState, useEffect, useCallback } from 'react'; +import { View, StyleSheet, ActivityIndicator, Text } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface CallScreenProps { + sessionId: string; + isAudioOnly?: boolean; + onCallEnd?: () => void; +} + +function CallScreen({ sessionId, isAudioOnly = false, onCallEnd }: CallScreenProps) { + const [callToken, setCallToken] = useState(null); + const [callSettings, setCallSettings] = useState(null); + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + const handleCallEnd = useCallback(() => { + console.log('Call ended'); + onCallEnd?.(); + }, [onCallEnd]); + + useEffect(() => { + async function initializeCall() { + try { + setIsLoading(true); + setError(null); + + // Generate call token + const { token } = await CometChatCalls.generateToken(sessionId); + setCallToken(token); + + // Create call listener + const listener = new CometChatCalls.OngoingCallListener({ + onUserJoined: (user) => { + console.log('User joined:', user.name); + }, + onUserLeft: (user) => { + console.log('User left:', user.name); + }, + onUserListUpdated: (userList) => { + console.log('Participants:', userList.length); + }, + onCallEnded: handleCallEnd, + onCallEndButtonPressed: () => { + console.log('End button pressed'); + CometChatCalls.leaveSession(); + }, + onSessionTimeout: () => { + console.log('Session timed out'); + handleCallEnd(); + }, + onError: (err) => { + console.error('Call error:', err); + setError(err.errorDescription); + }, + onRecordingStarted: () => { + console.log('Recording started'); + }, + onRecordingStopped: () => { + console.log('Recording stopped'); + }, + }); + + // Create call settings + const settings = new CometChatCalls.CallSettingsBuilder() + .enableDefaultLayout(true) + .setIsAudioOnlyCall(isAudioOnly) + .setMode(CometChatCalls.CALL_MODE.DEFAULT) + .showEndCallButton(true) + .showMuteAudioButton(true) + .showPauseVideoButton(!isAudioOnly) + .showSwitchCameraButton(!isAudioOnly) + .showAudioModeButton(true) + .startWithAudioMuted(false) + .startWithVideoMuted(false) + .setDefaultAudioMode(CometChatCalls.AUDIO_MODE.SPEAKER) + .showRecordingButton(true) + .setIdleTimeoutPeriod(180) + .setCallEventListener(listener) + .build(); + + setCallSettings(settings); + } catch (err: any) { + console.error('Failed to initialize call:', err); + setError(err.errorDescription || 'Failed to initialize call'); + } finally { + setIsLoading(false); + } + } + + initializeCall(); + }, [sessionId, isAudioOnly, handleCallEnd]); + + if (isLoading) { + return ( + + + Joining call... + + ); + } + + if (error) { + return ( + + {error} + + ); + } + + if (!callToken || !callSettings) { + return null; + } + + return ( + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#000', + }, + centered: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + backgroundColor: '#000', + }, + loadingText: { + color: '#fff', + marginTop: 16, + fontSize: 16, + }, + errorText: { + color: '#ff4444', + fontSize: 16, + textAlign: 'center', + paddingHorizontal: 20, + }, +}); + +export default CallScreen; +``` + +## Error Handling + +Common errors when joining a session: + +| Error Code | Description | +|------------|-------------| +| `ERROR_SESSION_ID_MISSING` | Session ID is empty or not provided | +| `ERROR_AUTH_TOKEN_MISSING` | User is not logged in or auth token is missing | +| `ERROR_SDK_NOT_INITIALIZED` | SDK not initialized. Call `init()` first | + +```tsx +try { + const { token } = await CometChatCalls.generateToken(sessionId); +} catch (error: any) { + switch (error.errorCode) { + case 'ERROR_SESSION_ID_MISSING': + console.error('Please provide a valid session ID'); + break; + case 'ERROR_AUTH_TOKEN_MISSING': + console.error('Please login before generating a token'); + break; + case 'ERROR_SDK_NOT_INITIALIZED': + console.error('Please initialize the SDK first'); + break; + default: + console.error('Error:', error.errorDescription); + } +} +``` + +## Related Documentation + +- [Session Settings](/calls/react-native/session-settings) - Configure call settings +- [Actions](/calls/react-native/actions) - Control the call programmatically +- [Events](/calls/react-native/events) - Handle call events diff --git a/calls/react-native/overview.mdx b/calls/react-native/overview.mdx index 599bf5a1..bc6906a3 100644 --- a/calls/react-native/overview.mdx +++ b/calls/react-native/overview.mdx @@ -3,4 +3,111 @@ title: "Calls SDK" sidebarTitle: "Overview" --- -Documentation coming soon for React Native Calls SDK. +The CometChat Calls SDK enables real-time voice and video calling capabilities in your React Native application. Built on top of WebRTC, it provides a complete calling solution with built-in UI components and extensive customization options. + + +**Faster Integration with UI Kits** + +If you're using CometChat UI Kits, voice and video calling is already integrated with ready-to-use components: +- Incoming & outgoing call screens +- Call buttons with one-tap calling +- Call logs with history + +👉 [React Native UI Kit Call Features](/ui-kit/react-native/call-features) + +Use this Calls SDK directly only if you need custom call UI or advanced control. + + +## Prerequisites + +Before integrating the Calls SDK, ensure you have: + +1. **CometChat Account**: [Sign up](https://app.cometchat.com/signup) and create an app to get your App ID, Region, and API Key +2. **CometChat Users**: Users must exist in CometChat to use calling features. For testing, create users via the [Dashboard](https://app.cometchat.com) or [REST API](/rest-api/chat-apis/users/create-user). Authentication is handled by the Calls SDK - see [Authentication](/calls/react-native/authentication) +3. **React Native Requirements**: + - React Native 0.71 or later + - Node.js 18 or later + - iOS: Minimum iOS 13.0, Xcode 14.0+ + - Android: Minimum SDK API Level 24 (Android 7.0) +4. **Permissions**: Camera and microphone permissions for video/audio calls + +## Call Flow + +```mermaid +sequenceDiagram + participant App + participant CometChatCalls + participant Component + + App->>CometChatCalls: init() + App->>CometChatCalls: login() + App->>CometChatCalls: generateToken() + App->>Component: Render with callToken + Component-->>App: Event callbacks + App->>CometChatCalls: Actions (mute, pause, etc.) + App->>CometChatCalls: leaveSession() +``` + +## Features + + + + + Incoming and outgoing call notifications with accept/reject functionality + + + + Tile, Sidebar, and Spotlight view modes for different call scenarios + + + + Switch between speaker, earpiece, Bluetooth, and headphones + + + + Record call sessions for later playback + + + + Retrieve call history and details + + + + Mute, pin, and manage call participants + + + + View screen shares from other participants + + + + Continue calls while using other apps + + + + Signal to get attention during calls + + + + Automatic session termination when alone in a call + + + + +## Architecture + +The SDK is organized around these core components: + +| Component | Description | +|-----------|-------------| +| `CometChatCalls` | Main entry point for SDK initialization, authentication, and session management | +| `CallAppSettingsBuilder` | Configuration builder for SDK initialization (App ID, Region) | +| `CallSettingsBuilder` | Configuration builder for individual call sessions | +| `CometChatCalls.Component` | React component that renders the call UI | +| `OngoingCallListener` | Event listener class for call events | + +## Related Documentation + +- [Setup](/calls/react-native/setup) - Install and configure the SDK +- [Authentication](/calls/react-native/authentication) - Initialize and authenticate users +- [Join Session](/calls/react-native/join-session) - Start and join calls diff --git a/calls/react-native/participant-management.mdx b/calls/react-native/participant-management.mdx new file mode 100644 index 00000000..43966933 --- /dev/null +++ b/calls/react-native/participant-management.mdx @@ -0,0 +1,349 @@ +--- +title: "Participant Management" +sidebarTitle: "Participant Management" +--- + +Manage call participants by muting, pinning, and monitoring their status during a call. + +## Get Participant List + +Listen for participant list changes: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +CometChatCalls.addEventListener('onParticipantListChanged', (participants) => { + console.log('Participants:', participants); + participants.forEach((participant) => { + console.log(`- ${participant.name} (${participant.uid})`); + }); +}); +``` + +## Participant Object + +Each participant contains: + +| Property | Type | Description | +|----------|------|-------------| +| `pid` | string | Participant ID (unique per session) | +| `uid` | string | User ID | +| `name` | string | Display name | +| `avatar` | string | Avatar URL (optional) | + +## Pin Participant + +Pin a participant to the main view in Sidebar or Spotlight layouts: + +```tsx +// Pin a participant's video +CometChatCalls.pinParticipant(participantId, 'human'); + +// Pin a participant's screen share +CometChatCalls.pinParticipant(participantId, 'screen-share'); +``` + +| Parameter | Type | Description | +|-----------|------|-------------| +| `participantId` | string | The participant's `pid` | +| `type` | string | `'human'` for video, `'screen-share'` for screen share | + +## Unpin Participant + +Remove the pinned participant: + +```tsx +CometChatCalls.unpinParticipant(); +``` + +## Mute Participant + +Mute another participant's audio (requires moderator permissions): + +```tsx +CometChatCalls.muteParticipant(participantId); +``` + +## Pause Participant Video + +Pause another participant's video (requires moderator permissions): + +```tsx +CometChatCalls.pauseParticipantVideo(participantId); +``` + +## Participant Events + +### Join and Leave + +```tsx +CometChatCalls.addEventListener('onParticipantJoined', (participant) => { + console.log(`${participant.name} joined the call`); +}); + +CometChatCalls.addEventListener('onParticipantLeft', (participant) => { + console.log(`${participant.name} left the call`); +}); +``` + +### Audio State + +```tsx +CometChatCalls.addEventListener('onParticipantAudioMuted', (participant) => { + console.log(`${participant.name} muted their audio`); +}); + +CometChatCalls.addEventListener('onParticipantAudioUnmuted', (participant) => { + console.log(`${participant.name} unmuted their audio`); +}); +``` + +### Video State + +```tsx +CometChatCalls.addEventListener('onParticipantVideoPaused', (participant) => { + console.log(`${participant.name} paused their video`); +}); + +CometChatCalls.addEventListener('onParticipantVideoResumed', (participant) => { + console.log(`${participant.name} resumed their video`); +}); +``` + +### Hand Raised + +```tsx +CometChatCalls.addEventListener('onParticipantHandRaised', (participant) => { + console.log(`${participant.name} raised their hand`); +}); + +CometChatCalls.addEventListener('onParticipantHandLowered', (participant) => { + console.log(`${participant.name} lowered their hand`); +}); +``` + +### Screen Sharing + +```tsx +CometChatCalls.addEventListener('onParticipantStartedScreenShare', (participant) => { + console.log(`${participant.name} started screen sharing`); +}); + +CometChatCalls.addEventListener('onParticipantStoppedScreenShare', (participant) => { + console.log(`${participant.name} stopped screen sharing`); +}); +``` + +### Dominant Speaker + +```tsx +CometChatCalls.addEventListener('onDominantSpeakerChanged', (participant) => { + console.log(`Dominant speaker: ${participant.name}`); +}); +``` + +## Using OngoingCallListener + +Handle participant events through the call settings listener: + +```tsx +const callListener = new CometChatCalls.OngoingCallListener({ + onUserJoined: (user) => { + console.log('User joined:', user.name); + }, + onUserLeft: (user) => { + console.log('User left:', user.name); + }, + onUserListUpdated: (userList) => { + console.log('Participant count:', userList.length); + }, + onUserMuted: (user) => { + console.log('User muted:', user.name); + }, +}); + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setCallEventListener(callListener) + .build(); +``` + +## Complete Example + +```tsx +import React, { useState, useEffect } from 'react'; +import { View, FlatList, Text, TouchableOpacity, StyleSheet } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface Participant { + pid: string; + uid: string; + name: string; + avatar?: string; +} + +function ParticipantList() { + const [participants, setParticipants] = useState([]); + const [pinnedId, setPinnedId] = useState(null); + + useEffect(() => { + const unsubscribeList = CometChatCalls.addEventListener( + 'onParticipantListChanged', + (list: Participant[]) => { + setParticipants(list); + } + ); + + const unsubscribeJoin = CometChatCalls.addEventListener( + 'onParticipantJoined', + (participant: Participant) => { + console.log(`${participant.name} joined`); + } + ); + + const unsubscribeLeave = CometChatCalls.addEventListener( + 'onParticipantLeft', + (participant: Participant) => { + console.log(`${participant.name} left`); + // Unpin if the pinned participant left + if (pinnedId === participant.pid) { + setPinnedId(null); + } + } + ); + + return () => { + unsubscribeList(); + unsubscribeJoin(); + unsubscribeLeave(); + }; + }, [pinnedId]); + + const handlePin = (participant: Participant) => { + if (pinnedId === participant.pid) { + CometChatCalls.unpinParticipant(); + setPinnedId(null); + } else { + CometChatCalls.pinParticipant(participant.pid, 'human'); + setPinnedId(participant.pid); + } + }; + + const handleMute = (participant: Participant) => { + CometChatCalls.muteParticipant(participant.pid); + }; + + const renderParticipant = ({ item }: { item: Participant }) => ( + + + + {item.name.charAt(0).toUpperCase()} + + + {item.name} + + handlePin(item)} + > + + {pinnedId === item.pid ? 'Unpin' : 'Pin'} + + + handleMute(item)} + > + Mute + + + + ); + + return ( + + + Participants ({participants.length}) + + item.pid} + renderItem={renderParticipant} + ListEmptyComponent={ + No other participants + } + /> + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#1a1a1a', + }, + header: { + color: '#fff', + fontSize: 18, + fontWeight: '600', + padding: 16, + borderBottomWidth: 1, + borderBottomColor: '#333', + }, + participantItem: { + flexDirection: 'row', + alignItems: 'center', + padding: 12, + borderBottomWidth: 1, + borderBottomColor: '#333', + }, + avatar: { + width: 40, + height: 40, + borderRadius: 20, + backgroundColor: '#6851D6', + justifyContent: 'center', + alignItems: 'center', + }, + avatarText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + name: { + flex: 1, + color: '#fff', + fontSize: 16, + marginLeft: 12, + }, + actions: { + flexDirection: 'row', + gap: 8, + }, + actionButton: { + backgroundColor: '#333', + paddingHorizontal: 12, + paddingVertical: 6, + borderRadius: 4, + }, + activeButton: { + backgroundColor: '#6851D6', + }, + actionText: { + color: '#fff', + fontSize: 12, + }, + emptyText: { + color: '#666', + textAlign: 'center', + padding: 20, + }, +}); + +export default ParticipantList; +``` + +## Related Documentation + +- [Events](/calls/react-native/events) - All participant events +- [Call Layouts](/calls/react-native/call-layouts) - Layout options for participants +- [Custom Participant List](/calls/react-native/custom-participant-list) - Build custom UI diff --git a/calls/react-native/picture-in-picture.mdx b/calls/react-native/picture-in-picture.mdx new file mode 100644 index 00000000..82fdb979 --- /dev/null +++ b/calls/react-native/picture-in-picture.mdx @@ -0,0 +1,239 @@ +--- +title: "Picture-in-Picture" +sidebarTitle: "Picture-in-Picture" +--- + +Picture-in-Picture (PiP) mode allows users to continue their call in a floating window while using other apps. This feature requires platform-specific configuration. + +## Enable Picture-in-Picture + +Enable PiP mode programmatically: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +CometChatCalls.enablePictureInPictureLayout(); +``` + +## Disable Picture-in-Picture + +Exit PiP mode and return to full-screen: + +```tsx +CometChatCalls.disablePictureInPictureLayout(); +``` + +## Listen for PiP Events + +```tsx +CometChatCalls.addEventListener('onPictureInPictureLayoutEnabled', () => { + console.log('PiP mode enabled'); +}); + +CometChatCalls.addEventListener('onPictureInPictureLayoutDisabled', () => { + console.log('PiP mode disabled'); +}); +``` + +## iOS Configuration + +### Enable Background Modes + +1. Open your project in Xcode +2. Select your target and go to **Signing & Capabilities** +3. Add **Background Modes** capability +4. Enable **Audio, AirPlay, and Picture in Picture** + +### Info.plist + +No additional Info.plist entries are required for PiP on iOS. + +### Automatic PiP + +iOS automatically enters PiP mode when: +- The user presses the home button during a video call +- The user swipes up to go to the app switcher + +## Android Configuration + +### Enable PiP Support + +Add PiP support to your main activity in `AndroidManifest.xml`: + +```xml + + + +``` + +### Minimum SDK + +PiP requires Android 8.0 (API level 26) or higher. Add to your `build.gradle`: + +```groovy +android { + defaultConfig { + minSdkVersion 26 + } +} +``` + +### Handle PiP Mode Changes + +In your React Native activity, handle PiP mode changes: + +```java +// MainActivity.java +@Override +public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode); + // Notify React Native about PiP state change +} +``` + +## Complete Example + +```tsx +import React, { useState, useEffect } from 'react'; +import { View, TouchableOpacity, Text, StyleSheet, AppState, Platform } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +function PictureInPictureControls() { + const [isPiPEnabled, setIsPiPEnabled] = useState(false); + const [appState, setAppState] = useState(AppState.currentState); + + useEffect(() => { + const unsubscribeEnabled = CometChatCalls.addEventListener( + 'onPictureInPictureLayoutEnabled', + () => { + setIsPiPEnabled(true); + } + ); + + const unsubscribeDisabled = CometChatCalls.addEventListener( + 'onPictureInPictureLayoutDisabled', + () => { + setIsPiPEnabled(false); + } + ); + + // Listen for app state changes + const subscription = AppState.addEventListener('change', (nextAppState) => { + if (appState.match(/active/) && nextAppState === 'background') { + // App going to background - enable PiP + CometChatCalls.enablePictureInPictureLayout(); + } + setAppState(nextAppState); + }); + + return () => { + unsubscribeEnabled(); + unsubscribeDisabled(); + subscription.remove(); + }; + }, [appState]); + + const togglePiP = () => { + if (isPiPEnabled) { + CometChatCalls.disablePictureInPictureLayout(); + } else { + CometChatCalls.enablePictureInPictureLayout(); + } + }; + + return ( + + + {isPiPEnabled ? 'Exit PiP' : 'Enter PiP'} + + + ); +} + +const styles = StyleSheet.create({ + button: { + backgroundColor: '#333', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 8, + }, + activeButton: { + backgroundColor: '#6851D6', + }, + buttonText: { + color: '#fff', + fontSize: 14, + fontWeight: '600', + }, +}); + +export default PictureInPictureControls; +``` + +## Auto-Enable PiP on Background + +Automatically enable PiP when the app goes to background: + +```tsx +import { useEffect, useRef } from 'react'; +import { AppState, AppStateStatus } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +function useAutoPiP(enabled: boolean = true) { + const appState = useRef(AppState.currentState); + + useEffect(() => { + if (!enabled) return; + + const subscription = AppState.addEventListener( + 'change', + (nextAppState: AppStateStatus) => { + if ( + appState.current.match(/active/) && + nextAppState === 'background' + ) { + CometChatCalls.enablePictureInPictureLayout(); + } else if ( + appState.current === 'background' && + nextAppState === 'active' + ) { + CometChatCalls.disablePictureInPictureLayout(); + } + appState.current = nextAppState; + } + ); + + return () => { + subscription.remove(); + }; + }, [enabled]); +} + +export default useAutoPiP; +``` + +## Platform Considerations + +### iOS + +- PiP is available on iOS 14.0 and later +- Works automatically when the app enters background +- User can resize and move the PiP window + +### Android + +- PiP requires Android 8.0 (API level 26) or higher +- Must be explicitly enabled in the manifest +- PiP window has fixed aspect ratio + +## Related Documentation + +- [Background Handling](/calls/react-native/background-handling) - Keep calls active in background +- [Actions](/calls/react-native/actions) - Control the call programmatically +- [Events](/calls/react-native/events) - PiP events diff --git a/calls/react-native/raise-hand.mdx b/calls/react-native/raise-hand.mdx new file mode 100644 index 00000000..8aa46aa1 --- /dev/null +++ b/calls/react-native/raise-hand.mdx @@ -0,0 +1,213 @@ +--- +title: "Raise Hand" +sidebarTitle: "Raise Hand" +--- + +The raise hand feature allows participants to signal that they want attention during a call, useful for Q&A sessions or large meetings. + +## Raise Hand + +Signal to other participants that you want attention: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +CometChatCalls.raiseHand(); +``` + +## Lower Hand + +Lower your raised hand: + +```tsx +CometChatCalls.lowerHand(); +``` + +## Listen for Hand Raised Events + +### Local Events + +Listen for when the raise hand button is clicked: + +```tsx +CometChatCalls.addEventListener('onRaiseHandButtonClicked', () => { + console.log('Raise hand button clicked'); +}); +``` + +### Participant Events + +Listen for when other participants raise or lower their hands: + +```tsx +CometChatCalls.addEventListener('onParticipantHandRaised', (participant) => { + console.log(`${participant.name} raised their hand`); +}); + +CometChatCalls.addEventListener('onParticipantHandLowered', (participant) => { + console.log(`${participant.name} lowered their hand`); +}); +``` + +## Complete Example + +```tsx +import React, { useState, useEffect } from 'react'; +import { View, TouchableOpacity, Text, StyleSheet, FlatList } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface Participant { + pid: string; + name: string; +} + +function RaiseHandFeature() { + const [isHandRaised, setIsHandRaised] = useState(false); + const [raisedHands, setRaisedHands] = useState([]); + + useEffect(() => { + const unsubscribeRaised = CometChatCalls.addEventListener( + 'onParticipantHandRaised', + (participant: Participant) => { + setRaisedHands((prev) => { + if (prev.find((p) => p.pid === participant.pid)) { + return prev; + } + return [...prev, participant]; + }); + } + ); + + const unsubscribeLowered = CometChatCalls.addEventListener( + 'onParticipantHandLowered', + (participant: Participant) => { + setRaisedHands((prev) => + prev.filter((p) => p.pid !== participant.pid) + ); + } + ); + + const unsubscribeLeft = CometChatCalls.addEventListener( + 'onParticipantLeft', + (participant: Participant) => { + setRaisedHands((prev) => + prev.filter((p) => p.pid !== participant.pid) + ); + } + ); + + return () => { + unsubscribeRaised(); + unsubscribeLowered(); + unsubscribeLeft(); + }; + }, []); + + const toggleHand = () => { + if (isHandRaised) { + CometChatCalls.lowerHand(); + setIsHandRaised(false); + } else { + CometChatCalls.raiseHand(); + setIsHandRaised(true); + } + }; + + const renderRaisedHand = ({ item }: { item: Participant }) => ( + + + {item.name} + + ); + + return ( + + + {isHandRaised ? '✋' : '🤚'} + + {isHandRaised ? 'Lower Hand' : 'Raise Hand'} + + + + {raisedHands.length > 0 && ( + + + Raised Hands ({raisedHands.length}) + + item.pid} + renderItem={renderRaisedHand} + horizontal + showsHorizontalScrollIndicator={false} + /> + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + padding: 16, + }, + button: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: '#333', + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 8, + gap: 8, + }, + activeButton: { + backgroundColor: '#f59e0b', + }, + buttonIcon: { + fontSize: 20, + }, + buttonText: { + color: '#fff', + fontSize: 14, + fontWeight: '600', + }, + raisedHandsList: { + marginTop: 16, + }, + listTitle: { + color: '#fff', + fontSize: 14, + fontWeight: '600', + marginBottom: 8, + }, + raisedHandItem: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#333', + paddingHorizontal: 12, + paddingVertical: 8, + borderRadius: 20, + marginRight: 8, + gap: 6, + }, + handIcon: { + fontSize: 16, + }, + participantName: { + color: '#fff', + fontSize: 14, + }, +}); + +export default RaiseHandFeature; +``` + +## Related Documentation + +- [Events](/calls/react-native/events) - All raise hand events +- [Participant Management](/calls/react-native/participant-management) - Manage participants +- [Actions](/calls/react-native/actions) - Available call actions diff --git a/calls/react-native/recording.mdx b/calls/react-native/recording.mdx new file mode 100644 index 00000000..4dfab3e8 --- /dev/null +++ b/calls/react-native/recording.mdx @@ -0,0 +1,242 @@ +--- +title: "Recording" +sidebarTitle: "Recording" +--- + +The CometChat Calls SDK supports recording call sessions. Recordings can be started manually or automatically when the call begins. + +## Enable Recording Button + +Show the recording button in the call UI: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .showRecordingButton(true) + .build(); +``` + +## Auto-Start Recording + +Start recording automatically when the call begins: + +```tsx +const callSettings = new CometChatCalls.CallSettingsBuilder() + .showRecordingButton(true) + .startRecordingOnCallStart(true) + .build(); +``` + +## Start Recording Programmatically + +Start recording during an active call: + +```tsx +CometChatCalls.startRecording(); +``` + +## Stop Recording + +Stop an active recording: + +```tsx +CometChatCalls.stopRecording(); +``` + +## Recording Events + +Listen for recording state changes: + +### Using OngoingCallListener + +```tsx +const callListener = new CometChatCalls.OngoingCallListener({ + onRecordingStarted: (data) => { + console.log('Recording started:', data); + }, + onRecordingStopped: (data) => { + console.log('Recording stopped:', data); + }, +}); + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setCallEventListener(callListener) + .build(); +``` + +### Using addEventListener + +```tsx +const unsubscribeStart = CometChatCalls.addEventListener( + 'onRecordingStarted', + () => { + console.log('Recording started'); + } +); + +const unsubscribeStop = CometChatCalls.addEventListener( + 'onRecordingStopped', + () => { + console.log('Recording stopped'); + } +); + +// Cleanup +unsubscribeStart(); +unsubscribeStop(); +``` + +### Participant Recording Events + +Listen when other participants start or stop recording: + +```tsx +CometChatCalls.addEventListener('onParticipantStartedRecording', (participant) => { + console.log(`${participant.name} started recording`); +}); + +CometChatCalls.addEventListener('onParticipantStoppedRecording', (participant) => { + console.log(`${participant.name} stopped recording`); +}); +``` + +## Recording Button Click Event + +Listen for when the recording button is clicked: + +```tsx +CometChatCalls.addEventListener('onRecordingToggleButtonClicked', () => { + console.log('Recording button clicked'); +}); +``` + +## Access Recordings + +Recordings are available through the call logs after the call ends. Use the Chat SDK to retrieve recordings: + +```tsx +import { CometChat } from '@cometchat/chat-sdk-react-native'; + +async function getCallRecordings(sessionId: string) { + const callLogRequest = new CometChat.CallLogRequestBuilder() + .setLimit(1) + .build(); + + try { + const callLogs = await callLogRequest.fetchNext(); + const callLog = callLogs.find((log) => log.sessionId === sessionId); + + if (callLog && callLog.recordings) { + console.log('Recordings:', callLog.recordings); + return callLog.recordings; + } + return []; + } catch (error) { + console.error('Error fetching recordings:', error); + throw error; + } +} +``` + +## Recording Object + +Each recording contains: + +| Property | Type | Description | +|----------|------|-------------| +| `rid` | string | Recording ID | +| `recordingUrl` | string | URL to download the recording | +| `startTime` | number | Recording start timestamp | +| `endTime` | number | Recording end timestamp | +| `duration` | number | Recording duration in seconds | + +## Complete Example + +```tsx +import React, { useState, useEffect } from 'react'; +import { View, TouchableOpacity, Text, StyleSheet } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +function RecordingControls() { + const [isRecording, setIsRecording] = useState(false); + + useEffect(() => { + const unsubscribeStart = CometChatCalls.addEventListener( + 'onRecordingStarted', + () => { + setIsRecording(true); + } + ); + + const unsubscribeStop = CometChatCalls.addEventListener( + 'onRecordingStopped', + () => { + setIsRecording(false); + } + ); + + return () => { + unsubscribeStart(); + unsubscribeStop(); + }; + }, []); + + const toggleRecording = () => { + if (isRecording) { + CometChatCalls.stopRecording(); + } else { + CometChatCalls.startRecording(); + } + }; + + return ( + + + + {isRecording ? 'Stop Recording' : 'Start Recording'} + + + ); +} + +const styles = StyleSheet.create({ + button: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#333', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 8, + gap: 8, + }, + recordingButton: { + backgroundColor: '#ff4444', + }, + indicator: { + width: 12, + height: 12, + borderRadius: 6, + backgroundColor: '#666', + }, + recordingIndicator: { + backgroundColor: '#fff', + }, + buttonText: { + color: '#fff', + fontSize: 14, + fontWeight: '600', + }, +}); + +export default RecordingControls; +``` + +## Related Documentation + +- [Call Logs](/calls/react-native/call-logs) - Access call history and recordings +- [Session Settings](/calls/react-native/session-settings) - Configure recording options +- [Events](/calls/react-native/events) - Handle recording events diff --git a/calls/react-native/ringing.mdx b/calls/react-native/ringing.mdx new file mode 100644 index 00000000..5f83dea0 --- /dev/null +++ b/calls/react-native/ringing.mdx @@ -0,0 +1,394 @@ +--- +title: "Ringing" +sidebarTitle: "Ringing" +--- + +Implement incoming and outgoing call notifications using the CometChat Chat SDK's calling features. The Calls SDK handles the actual call session, while the Chat SDK manages call signaling. + + +Ringing functionality requires the CometChat Chat SDK (`@cometchat/chat-sdk-react-native`) for call signaling. The Calls SDK is used for the actual call session. + + +## Prerequisites + +1. CometChat Chat SDK integrated +2. CometChat Calls SDK integrated +3. Push notifications configured (for background calls) + +## Initiate a Call + +Use the Chat SDK to initiate a call: + +```tsx +import { CometChat } from '@cometchat/chat-sdk-react-native'; + +async function initiateCall(receiverId: string, callType: string, receiverType: string) { + const call = new CometChat.Call(receiverId, callType, receiverType); + + try { + const outgoingCall = await CometChat.initiateCall(call); + console.log('Call initiated:', outgoingCall); + return outgoingCall; + } catch (error) { + console.error('Error initiating call:', error); + throw error; + } +} + +// Initiate a video call to a user +initiateCall('user_uid', CometChat.CALL_TYPE.VIDEO, CometChat.RECEIVER_TYPE.USER); + +// Initiate an audio call to a group +initiateCall('group_guid', CometChat.CALL_TYPE.AUDIO, CometChat.RECEIVER_TYPE.GROUP); +``` + +## Listen for Incoming Calls + +Register a call listener to receive incoming calls: + +```tsx +import { CometChat } from '@cometchat/chat-sdk-react-native'; + +const listenerId = 'unique_listener_id'; + +CometChat.addCallListener( + listenerId, + new CometChat.CallListener({ + onIncomingCallReceived: (call) => { + console.log('Incoming call:', call); + // Show incoming call UI + }, + onOutgoingCallAccepted: (call) => { + console.log('Call accepted:', call); + // Start the call session + }, + onOutgoingCallRejected: (call) => { + console.log('Call rejected:', call); + // Handle rejection + }, + onIncomingCallCancelled: (call) => { + console.log('Call cancelled:', call); + // Dismiss incoming call UI + }, + onCallEndedMessageReceived: (call) => { + console.log('Call ended:', call); + // Handle call end + }, + }) +); + +// Remove listener when done +CometChat.removeCallListener(listenerId); +``` + +## Accept a Call + +Accept an incoming call: + +```tsx +async function acceptCall(sessionId: string) { + try { + const call = await CometChat.acceptCall(sessionId); + console.log('Call accepted:', call); + // Start the call session with Calls SDK + return call; + } catch (error) { + console.error('Error accepting call:', error); + throw error; + } +} +``` + +## Reject a Call + +Reject an incoming call: + +```tsx +async function rejectCall(sessionId: string, status: string) { + try { + const call = await CometChat.rejectCall(sessionId, status); + console.log('Call rejected:', call); + return call; + } catch (error) { + console.error('Error rejecting call:', error); + throw error; + } +} + +// Reject with status +rejectCall(sessionId, CometChat.CALL_STATUS.REJECTED); + +// Mark as busy +rejectCall(sessionId, CometChat.CALL_STATUS.BUSY); +``` + +## End a Call + +End an ongoing call: + +```tsx +async function endCall(sessionId: string) { + try { + // End the call session + CometChatCalls.leaveSession(); + + // Notify other participants via Chat SDK + const call = await CometChat.endCall(sessionId); + console.log('Call ended:', call); + return call; + } catch (error) { + console.error('Error ending call:', error); + throw error; + } +} +``` + +## Start Call Session After Accept + +After accepting a call, start the Calls SDK session: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +async function startCallSession(call: any) { + const sessionId = call.getSessionId(); + + try { + // Generate token for the session + const { token } = await CometChatCalls.generateToken(sessionId); + + // Create call settings + const isAudioOnly = call.getType() === CometChat.CALL_TYPE.AUDIO; + + const callSettings = new CometChatCalls.CallSettingsBuilder() + .setIsAudioOnlyCall(isAudioOnly) + .setCallEventListener(new CometChatCalls.OngoingCallListener({ + onCallEnded: () => { + console.log('Call ended'); + }, + })) + .build(); + + return { token, callSettings }; + } catch (error) { + console.error('Error starting call session:', error); + throw error; + } +} +``` + +## Complete Example + +```tsx +import React, { useState, useEffect, useCallback } from 'react'; +import { View, Text, TouchableOpacity, StyleSheet, Modal } from 'react-native'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface Call { + getSessionId: () => string; + getType: () => string; + getSender: () => { getName: () => string }; +} + +function CallManager({ children }: { children: React.ReactNode }) { + const [incomingCall, setIncomingCall] = useState(null); + const [activeCall, setActiveCall] = useState<{ + token: string; + settings: any; + } | null>(null); + + useEffect(() => { + const listenerId = 'call_manager_listener'; + + CometChat.addCallListener( + listenerId, + new CometChat.CallListener({ + onIncomingCallReceived: (call: Call) => { + setIncomingCall(call); + }, + onOutgoingCallAccepted: async (call: Call) => { + await startSession(call); + }, + onOutgoingCallRejected: () => { + setActiveCall(null); + }, + onIncomingCallCancelled: () => { + setIncomingCall(null); + }, + onCallEndedMessageReceived: () => { + setActiveCall(null); + setIncomingCall(null); + }, + }) + ); + + return () => { + CometChat.removeCallListener(listenerId); + }; + }, []); + + const startSession = async (call: Call) => { + try { + const sessionId = call.getSessionId(); + const { token } = await CometChatCalls.generateToken(sessionId); + + const isAudioOnly = call.getType() === CometChat.CALL_TYPE.AUDIO; + + const settings = new CometChatCalls.CallSettingsBuilder() + .setIsAudioOnlyCall(isAudioOnly) + .setCallEventListener(new CometChatCalls.OngoingCallListener({ + onCallEnded: () => { + setActiveCall(null); + }, + })) + .build(); + + setActiveCall({ token, settings }); + setIncomingCall(null); + } catch (error) { + console.error('Error starting session:', error); + } + }; + + const handleAccept = async () => { + if (!incomingCall) return; + + try { + const call = await CometChat.acceptCall(incomingCall.getSessionId()); + await startSession(call); + } catch (error) { + console.error('Error accepting call:', error); + } + }; + + const handleReject = async () => { + if (!incomingCall) return; + + try { + await CometChat.rejectCall( + incomingCall.getSessionId(), + CometChat.CALL_STATUS.REJECTED + ); + setIncomingCall(null); + } catch (error) { + console.error('Error rejecting call:', error); + } + }; + + const handleEndCall = async () => { + if (!activeCall) return; + + CometChatCalls.leaveSession(); + setActiveCall(null); + }; + + return ( + + {children} + + {/* Incoming Call Modal */} + + + + + {incomingCall?.getSender().getName()} + + + Incoming {incomingCall?.getType()} call + + + + Decline + + + Accept + + + + + + + {/* Active Call */} + {activeCall && ( + + + + + + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + }, + modalOverlay: { + flex: 1, + backgroundColor: 'rgba(0, 0, 0, 0.8)', + justifyContent: 'center', + alignItems: 'center', + }, + incomingCallCard: { + backgroundColor: '#1a1a1a', + borderRadius: 20, + padding: 30, + alignItems: 'center', + width: '80%', + }, + callerName: { + color: '#fff', + fontSize: 24, + fontWeight: '600', + marginBottom: 8, + }, + callType: { + color: '#999', + fontSize: 16, + marginBottom: 30, + }, + callActions: { + flexDirection: 'row', + gap: 20, + }, + callButton: { + paddingHorizontal: 30, + paddingVertical: 15, + borderRadius: 30, + }, + rejectButton: { + backgroundColor: '#ff4444', + }, + acceptButton: { + backgroundColor: '#22c55e', + }, + buttonText: { + color: '#fff', + fontSize: 16, + fontWeight: '600', + }, + callContainer: { + flex: 1, + backgroundColor: '#000', + }, +}); + +export default CallManager; +``` + +## Related Documentation + +- [VoIP Calling](/calls/react-native/voip-calling) - Push notifications for calls +- [Join Session](/calls/react-native/join-session) - Start call sessions +- [Background Handling](/calls/react-native/background-handling) - Handle background calls diff --git a/calls/react-native/screen-sharing.mdx b/calls/react-native/screen-sharing.mdx new file mode 100644 index 00000000..2b48c17b --- /dev/null +++ b/calls/react-native/screen-sharing.mdx @@ -0,0 +1,252 @@ +--- +title: "Screen Sharing" +sidebarTitle: "Screen Sharing" +--- + +The CometChat Calls SDK supports viewing screen shares from other participants. Screen sharing initiation on React Native requires platform-specific configuration. + +## View Screen Shares + +When a participant shares their screen, the SDK automatically displays it. Listen for screen share events: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +CometChatCalls.addEventListener('onParticipantStartedScreenShare', (participant) => { + console.log(`${participant.name} started screen sharing`); +}); + +CometChatCalls.addEventListener('onParticipantStoppedScreenShare', (participant) => { + console.log(`${participant.name} stopped screen sharing`); +}); +``` + +## Start Screen Sharing + +Start sharing your screen: + +```tsx +CometChatCalls.startScreenSharing(); +``` + +## Stop Screen Sharing + +Stop sharing your screen: + +```tsx +CometChatCalls.stopScreenSharing(); +``` + +## Screen Share Events + +### Local Screen Share + +```tsx +CometChatCalls.addEventListener('onScreenShareStarted', () => { + console.log('Screen sharing started'); +}); + +CometChatCalls.addEventListener('onScreenShareStopped', () => { + console.log('Screen sharing stopped'); +}); +``` + +### Button Click Event + +```tsx +CometChatCalls.addEventListener('onScreenShareButtonClicked', () => { + console.log('Screen share button clicked'); +}); +``` + +## iOS Configuration + +### Enable Broadcast Upload Extension + +To enable screen sharing on iOS, you need to add a Broadcast Upload Extension: + +1. In Xcode, go to **File > New > Target** +2. Select **Broadcast Upload Extension** +3. Name it (e.g., `ScreenShareExtension`) +4. Set the same Team and Bundle Identifier prefix as your main app + +### Configure App Groups + +1. Select your main app target +2. Go to **Signing & Capabilities** +3. Add **App Groups** capability +4. Create a new group (e.g., `group.com.yourcompany.yourapp`) +5. Repeat for the Broadcast Upload Extension target + +### Update Info.plist + +Add to your extension's `Info.plist`: + +```xml +NSExtension + + NSExtensionPointIdentifier + com.apple.broadcast-services-upload + NSExtensionPrincipalClass + SampleHandler + RPBroadcastProcessMode + RPBroadcastProcessModeSampleBuffer + +``` + +## Android Configuration + +### Add Permissions + +Add to your `AndroidManifest.xml`: + +```xml + + +``` + +### Configure Foreground Service + +For Android 10+, screen sharing requires a foreground service. Add to your `AndroidManifest.xml`: + +```xml + +``` + +## Pin Screen Share + +Pin a participant's screen share to the main view: + +```tsx +CometChatCalls.pinParticipant(participantId, 'screen-share'); +``` + +## Layout Behavior + +When screen sharing starts: +- The SDK automatically switches to Sidebar layout +- The screen share is displayed in the main view +- Other participants appear in the sidebar + +When screen sharing stops: +- The layout returns to the previous state (if it was programmatically set) + +## Complete Example + +```tsx +import React, { useState, useEffect } from 'react'; +import { View, TouchableOpacity, Text, StyleSheet, Alert } from 'react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +function ScreenShareControls() { + const [isSharing, setIsSharing] = useState(false); + const [remoteSharer, setRemoteSharer] = useState(null); + + useEffect(() => { + const unsubscribeStart = CometChatCalls.addEventListener( + 'onScreenShareStarted', + () => { + setIsSharing(true); + } + ); + + const unsubscribeStop = CometChatCalls.addEventListener( + 'onScreenShareStopped', + () => { + setIsSharing(false); + } + ); + + const unsubscribeRemoteStart = CometChatCalls.addEventListener( + 'onParticipantStartedScreenShare', + (participant) => { + setRemoteSharer(participant.name); + } + ); + + const unsubscribeRemoteStop = CometChatCalls.addEventListener( + 'onParticipantStoppedScreenShare', + () => { + setRemoteSharer(null); + } + ); + + return () => { + unsubscribeStart(); + unsubscribeStop(); + unsubscribeRemoteStart(); + unsubscribeRemoteStop(); + }; + }, []); + + const toggleScreenShare = () => { + if (isSharing) { + CometChatCalls.stopScreenSharing(); + } else { + if (remoteSharer) { + Alert.alert( + 'Screen Share Active', + `${remoteSharer} is currently sharing their screen.` + ); + return; + } + CometChatCalls.startScreenSharing(); + } + }; + + return ( + + {remoteSharer && ( + + {remoteSharer} is sharing their screen + + )} + + + {isSharing ? 'Stop Sharing' : 'Share Screen'} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + padding: 16, + alignItems: 'center', + }, + sharingText: { + color: '#6851D6', + fontSize: 14, + marginBottom: 8, + }, + button: { + backgroundColor: '#333', + paddingHorizontal: 20, + paddingVertical: 12, + borderRadius: 8, + }, + activeButton: { + backgroundColor: '#6851D6', + }, + buttonText: { + color: '#fff', + fontSize: 14, + fontWeight: '600', + }, +}); + +export default ScreenShareControls; +``` + +## Related Documentation + +- [Call Layouts](/calls/react-native/call-layouts) - Layout behavior during screen share +- [Participant Management](/calls/react-native/participant-management) - Pin screen shares +- [Events](/calls/react-native/events) - Screen share events diff --git a/calls/react-native/session-settings.mdx b/calls/react-native/session-settings.mdx new file mode 100644 index 00000000..4102e94f --- /dev/null +++ b/calls/react-native/session-settings.mdx @@ -0,0 +1,261 @@ +--- +title: "Session Settings" +sidebarTitle: "Session Settings" +--- + +Configure call sessions using the `CallSettingsBuilder` class. This allows you to customize the call UI, behavior, and event handling. + +## CallSettingsBuilder + +The `CallSettingsBuilder` provides a fluent API to configure call sessions: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .enableDefaultLayout(true) + .setIsAudioOnlyCall(false) + .setMode(CometChatCalls.CALL_MODE.DEFAULT) + .showEndCallButton(true) + .showMuteAudioButton(true) + .showPauseVideoButton(true) + .showSwitchCameraButton(true) + .showAudioModeButton(true) + .startWithAudioMuted(false) + .startWithVideoMuted(false) + .setDefaultAudioMode(CometChatCalls.AUDIO_MODE.SPEAKER) + .showRecordingButton(false) + .startRecordingOnCallStart(false) + .setIdleTimeoutPeriod(180) + .enableVideoTileClick(true) + .enableVideoTileDrag(true) + .setCallEventListener(callListener) + .build(); +``` + +## Configuration Options + +### Call Type + +| Method | Type | Default | Description | +|--------|------|---------|-------------| +| `setIsAudioOnlyCall(isAudioOnly)` | boolean | `false` | Set to `true` for audio-only calls | + +```tsx +// Audio-only call +const audioCallSettings = new CometChatCalls.CallSettingsBuilder() + .setIsAudioOnlyCall(true) + .build(); + +// Video call (default) +const videoCallSettings = new CometChatCalls.CallSettingsBuilder() + .setIsAudioOnlyCall(false) + .build(); +``` + +### Layout Mode + +| Method | Type | Default | Description | +|--------|------|---------|-------------| +| `setMode(mode)` | string | `DEFAULT` | Call layout mode | + +Available modes: + +| Mode | Description | +|------|-------------| +| `CometChatCalls.CALL_MODE.DEFAULT` | Grid layout with all participants visible | +| `CometChatCalls.CALL_MODE.SPOTLIGHT` | Focus on one participant with others in sidebar | + +```tsx +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setMode(CometChatCalls.CALL_MODE.SPOTLIGHT) + .build(); +``` + +### UI Controls + +| Method | Type | Default | Description | +|--------|------|---------|-------------| +| `enableDefaultLayout(enabled)` | boolean | `true` | Show/hide the default button layout | +| `showEndCallButton(show)` | boolean | `true` | Show/hide the end call button | +| `showMuteAudioButton(show)` | boolean | `true` | Show/hide the mute audio button | +| `showPauseVideoButton(show)` | boolean | `true` | Show/hide the pause video button | +| `showSwitchCameraButton(show)` | boolean | `true` | Show/hide the switch camera button | +| `showAudioModeButton(show)` | boolean | `true` | Show/hide the audio mode button | +| `showRecordingButton(show)` | boolean | `false` | Show/hide the recording button | + +```tsx +const callSettings = new CometChatCalls.CallSettingsBuilder() + .enableDefaultLayout(true) + .showEndCallButton(true) + .showMuteAudioButton(true) + .showPauseVideoButton(true) + .showSwitchCameraButton(true) + .showAudioModeButton(true) + .showRecordingButton(true) + .build(); +``` + +### Initial State + +| Method | Type | Default | Description | +|--------|------|---------|-------------| +| `startWithAudioMuted(muted)` | boolean | `false` | Start call with audio muted | +| `startWithVideoMuted(muted)` | boolean | `false` | Start call with video paused | +| `setDefaultAudioMode(mode)` | string | - | Set the default audio output mode | + +Available audio modes: + +| Mode | Description | +|------|-------------| +| `CometChatCalls.AUDIO_MODE.SPEAKER` | Phone speaker | +| `CometChatCalls.AUDIO_MODE.EARPIECE` | Phone earpiece | +| `CometChatCalls.AUDIO_MODE.BLUETOOTH` | Bluetooth device | +| `CometChatCalls.AUDIO_MODE.HEADPHONES` | Wired headphones | + +```tsx +const callSettings = new CometChatCalls.CallSettingsBuilder() + .startWithAudioMuted(true) + .startWithVideoMuted(false) + .setDefaultAudioMode(CometChatCalls.AUDIO_MODE.SPEAKER) + .build(); +``` + +### Recording + +| Method | Type | Default | Description | +|--------|------|---------|-------------| +| `showRecordingButton(show)` | boolean | `false` | Show/hide the recording button | +| `startRecordingOnCallStart(start)` | boolean | `false` | Auto-start recording when call begins | + +```tsx +const callSettings = new CometChatCalls.CallSettingsBuilder() + .showRecordingButton(true) + .startRecordingOnCallStart(true) + .build(); +``` + +### Idle Timeout + +| Method | Type | Default | Description | +|--------|------|---------|-------------| +| `setIdleTimeoutPeriod(seconds)` | number | `180` | Seconds before auto-ending when alone | + +```tsx +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setIdleTimeoutPeriod(300) // 5 minutes + .build(); +``` + +### Video Tile Interaction + +| Method | Type | Default | Description | +|--------|------|---------|-------------| +| `enableVideoTileClick(enabled)` | boolean | `true` | Enable clicking video tiles in Spotlight mode | +| `enableVideoTileDrag(enabled)` | boolean | `true` | Enable dragging video tiles in Spotlight mode | + +```tsx +const callSettings = new CometChatCalls.CallSettingsBuilder() + .enableVideoTileClick(true) + .enableVideoTileDrag(true) + .build(); +``` + +## Event Listener + +Set up an event listener to handle call events: + +```tsx +const callListener = new CometChatCalls.OngoingCallListener({ + onUserJoined: (user) => { + console.log('User joined:', user); + }, + onUserLeft: (user) => { + console.log('User left:', user); + }, + onUserListUpdated: (userList) => { + console.log('User list updated:', userList); + }, + onCallEnded: () => { + console.log('Call ended'); + }, + onCallEndButtonPressed: () => { + console.log('End call button pressed'); + }, + onError: (error) => { + console.error('Call error:', error); + }, + onAudioModesUpdated: (audioModes) => { + console.log('Audio modes updated:', audioModes); + }, + onRecordingStarted: (data) => { + console.log('Recording started:', data); + }, + onRecordingStopped: (data) => { + console.log('Recording stopped:', data); + }, + onUserMuted: (user) => { + console.log('User muted:', user); + }, + onSessionTimeout: () => { + console.log('Session timed out'); + }, +}); + +const callSettings = new CometChatCalls.CallSettingsBuilder() + .setCallEventListener(callListener) + .build(); +``` + +## Complete Example + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +function createCallSettings(isAudioOnly: boolean = false) { + const callListener = new CometChatCalls.OngoingCallListener({ + onUserJoined: (user) => { + console.log('User joined:', user.name); + }, + onUserLeft: (user) => { + console.log('User left:', user.name); + }, + onCallEnded: () => { + console.log('Call ended'); + // Navigate back or update UI + }, + onError: (error) => { + console.error('Call error:', error.errorDescription); + }, + }); + + return new CometChatCalls.CallSettingsBuilder() + .enableDefaultLayout(true) + .setIsAudioOnlyCall(isAudioOnly) + .setMode(CometChatCalls.CALL_MODE.DEFAULT) + .showEndCallButton(true) + .showMuteAudioButton(true) + .showPauseVideoButton(!isAudioOnly) + .showSwitchCameraButton(!isAudioOnly) + .showAudioModeButton(true) + .startWithAudioMuted(false) + .startWithVideoMuted(false) + .setDefaultAudioMode(CometChatCalls.AUDIO_MODE.SPEAKER) + .showRecordingButton(true) + .setIdleTimeoutPeriod(180) + .setCallEventListener(callListener) + .build(); +} + +// Create video call settings +const videoCallSettings = createCallSettings(false); + +// Create audio call settings +const audioCallSettings = createCallSettings(true); +``` + +## Related Documentation + +- [Join Session](/calls/react-native/join-session) - Start a call with these settings +- [Events](/calls/react-native/events) - Detailed event documentation +- [Actions](/calls/react-native/actions) - Control the call programmatically diff --git a/calls/react-native/setup.mdx b/calls/react-native/setup.mdx new file mode 100644 index 00000000..4924117d --- /dev/null +++ b/calls/react-native/setup.mdx @@ -0,0 +1,143 @@ +--- +title: "Setup" +sidebarTitle: "Setup" +--- + +This guide walks you through installing the CometChat Calls SDK and configuring it in your React Native application. + +## Add the CometChat Dependency + +### Using npm + +```bash +npm install @cometchat/calls-sdk-react-native +``` + +### Using Yarn + +```bash +yarn add @cometchat/calls-sdk-react-native +``` + +## iOS Configuration + +### Install CocoaPods Dependencies + +Navigate to your iOS directory and install the pods: + +```bash +cd ios +pod install +cd .. +``` + +### Add Permissions + +Add the required permissions to your `ios/YourApp/Info.plist`: + +```xml +NSCameraUsageDescription +Camera access is required for video calls +NSMicrophoneUsageDescription +Microphone access is required for voice and video calls +``` + +### Enable Background Modes + +For calls to continue when the app is in the background: + +1. Open your project in Xcode +2. Select your target and go to **Signing & Capabilities** +3. Click **+ Capability** and add **Background Modes** +4. Enable: + - **Audio, AirPlay, and Picture in Picture** + - **Voice over IP** (if using VoIP push notifications) + +## Android Configuration + +### Add Repository + +Add the CometChat repository to your **project level** `android/build.gradle`: + +```groovy +allprojects { + repositories { + google() + mavenCentral() + maven { + url "https://dl.cloudsmith.io/public/cometchat/cometchat/maven/" + } + } +} +``` + +### Configure Java Version + +Add Java 8 compatibility to your **app level** `android/app/build.gradle`: + +```groovy +android { + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} +``` + +### Add Permissions + +Add the required permissions to your `android/app/src/main/AndroidManifest.xml`: + +```xml + + + + + + + +``` + + +For Android 6.0 (API level 23) and above, you must request camera and microphone permissions at runtime before starting a call. + + +### Request Runtime Permissions + +Use a library like `react-native-permissions` or implement native permission requests: + +```tsx +import { PermissionsAndroid, Platform } from 'react-native'; + +async function requestCallPermissions(): Promise { + if (Platform.OS === 'android') { + const granted = await PermissionsAndroid.requestMultiple([ + PermissionsAndroid.PERMISSIONS.CAMERA, + PermissionsAndroid.PERMISSIONS.RECORD_AUDIO, + ]); + + return ( + granted['android.permission.CAMERA'] === PermissionsAndroid.RESULTS.GRANTED && + granted['android.permission.RECORD_AUDIO'] === PermissionsAndroid.RESULTS.GRANTED + ); + } + return true; +} +``` + +## Verify Installation + +After installation, rebuild your app: + +```bash +# iOS +npx react-native run-ios + +# Android +npx react-native run-android +``` + +## Related Documentation + +- [Authentication](/calls/react-native/authentication) - Initialize the SDK and authenticate users +- [Join Session](/calls/react-native/join-session) - Start your first call diff --git a/calls/react-native/share-invite.mdx b/calls/react-native/share-invite.mdx new file mode 100644 index 00000000..c1f04d6c --- /dev/null +++ b/calls/react-native/share-invite.mdx @@ -0,0 +1,360 @@ +--- +title: "Share Invite" +sidebarTitle: "Share Invite" +--- + +Allow users to share call invite links with others. When the share invite button is clicked, you can generate and share a link that others can use to join the call. + +## Listen for Share Invite Button Click + +Handle when the user clicks the share invite button: + +```tsx +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +CometChatCalls.addEventListener('onShareInviteButtonClicked', () => { + console.log('Share invite button clicked'); + // Generate and share invite link +}); +``` + +## Generate Invite Link + +Create an invite link that others can use to join the call: + +```tsx +function generateInviteLink(sessionId: string): string { + // Use your app's deep link scheme + const baseUrl = 'https://yourapp.com/call'; + return `${baseUrl}?sessionId=${encodeURIComponent(sessionId)}`; +} +``` + +## Share Using React Native Share + +Use the built-in Share API to share the invite: + +```tsx +import { Share } from 'react-native'; + +async function shareInvite(sessionId: string) { + const inviteLink = generateInviteLink(sessionId); + + try { + const result = await Share.share({ + message: `Join my call: ${inviteLink}`, + title: 'Join Call', + url: inviteLink, // iOS only + }); + + if (result.action === Share.sharedAction) { + if (result.activityType) { + console.log('Shared with activity type:', result.activityType); + } else { + console.log('Shared successfully'); + } + } else if (result.action === Share.dismissedAction) { + console.log('Share dismissed'); + } + } catch (error) { + console.error('Error sharing:', error); + } +} +``` + +## Handle Deep Links + +Configure your app to handle incoming deep links: + +### iOS Configuration + +Add URL scheme to `ios/YourApp/Info.plist`: + +```xml +CFBundleURLTypes + + + CFBundleURLSchemes + + yourapp + + + +``` + +### Android Configuration + +Add intent filter to `android/app/src/main/AndroidManifest.xml`: + +```xml + + + + + + + + + +``` + +### Handle Incoming Links + +```tsx +import { useEffect } from 'react'; +import { Linking } from 'react-native'; + +function useDeepLinks(onJoinCall: (sessionId: string) => void) { + useEffect(() => { + // Handle initial URL (app opened via link) + Linking.getInitialURL().then((url) => { + if (url) { + handleDeepLink(url); + } + }); + + // Handle URL when app is already open + const subscription = Linking.addEventListener('url', (event) => { + handleDeepLink(event.url); + }); + + return () => { + subscription.remove(); + }; + }, []); + + const handleDeepLink = (url: string) => { + try { + const parsedUrl = new URL(url); + const sessionId = parsedUrl.searchParams.get('sessionId'); + + if (sessionId) { + onJoinCall(sessionId); + } + } catch (error) { + console.error('Error parsing deep link:', error); + } + }; +} + +export default useDeepLinks; +``` + +## Complete Example + +```tsx +import React, { useState, useEffect, useCallback } from 'react'; +import { View, TouchableOpacity, Text, StyleSheet, Share, Alert } from 'react-native'; +import Clipboard from '@react-native-clipboard/clipboard'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +interface ShareInviteProps { + sessionId: string; +} + +function ShareInvite({ sessionId }: ShareInviteProps) { + const [showOptions, setShowOptions] = useState(false); + + useEffect(() => { + const unsubscribe = CometChatCalls.addEventListener( + 'onShareInviteButtonClicked', + () => { + setShowOptions(true); + } + ); + + return () => unsubscribe(); + }, []); + + const generateInviteLink = useCallback(() => { + return `https://yourapp.com/call?sessionId=${encodeURIComponent(sessionId)}`; + }, [sessionId]); + + const handleShare = async () => { + const inviteLink = generateInviteLink(); + + try { + await Share.share({ + message: `Join my video call!\n\n${inviteLink}`, + title: 'Join Call', + }); + } catch (error) { + console.error('Error sharing:', error); + } + + setShowOptions(false); + }; + + const handleCopyLink = () => { + const inviteLink = generateInviteLink(); + Clipboard.setString(inviteLink); + + Alert.alert('Link Copied', 'The invite link has been copied to your clipboard.'); + setShowOptions(false); + }; + + if (!showOptions) { + return ( + setShowOptions(true)} + > + 🔗 Share + + ); + } + + return ( + + + Share Invite + + + 📤 + Share via... + + + + 📋 + Copy Link + + + setShowOptions(false)} + > + Cancel + + + + ); +} + +const styles = StyleSheet.create({ + shareButton: { + backgroundColor: '#333', + paddingHorizontal: 16, + paddingVertical: 12, + borderRadius: 8, + }, + shareButtonText: { + color: '#fff', + fontSize: 14, + fontWeight: '600', + }, + optionsContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + bottom: 0, + backgroundColor: 'rgba(0, 0, 0, 0.5)', + justifyContent: 'center', + alignItems: 'center', + }, + optionsCard: { + backgroundColor: '#1a1a1a', + borderRadius: 16, + padding: 20, + width: '80%', + maxWidth: 300, + }, + optionsTitle: { + color: '#fff', + fontSize: 18, + fontWeight: '600', + textAlign: 'center', + marginBottom: 20, + }, + optionButton: { + flexDirection: 'row', + alignItems: 'center', + backgroundColor: '#333', + padding: 16, + borderRadius: 8, + marginBottom: 12, + }, + optionIcon: { + fontSize: 20, + marginRight: 12, + }, + optionText: { + color: '#fff', + fontSize: 16, + }, + cancelButton: { + padding: 16, + alignItems: 'center', + }, + cancelText: { + color: '#6851D6', + fontSize: 16, + fontWeight: '600', + }, +}); + +export default ShareInvite; +``` + +## Universal Links (iOS) + +For a better user experience, configure Universal Links: + +1. Create an `apple-app-site-association` file on your server: + +```json +{ + "applinks": { + "apps": [], + "details": [ + { + "appID": "TEAM_ID.com.yourcompany.yourapp", + "paths": ["/call/*"] + } + ] + } +} +``` + +2. Host it at `https://yourapp.com/.well-known/apple-app-site-association` + +3. Add Associated Domains capability in Xcode: + - `applinks:yourapp.com` + +## App Links (Android) + +Configure App Links for Android: + +1. Create a `assetlinks.json` file: + +```json +[{ + "relation": ["delegate_permission/common.handle_all_urls"], + "target": { + "namespace": "android_app", + "package_name": "com.yourcompany.yourapp", + "sha256_cert_fingerprints": ["YOUR_SHA256_FINGERPRINT"] + } +}] +``` + +2. Host it at `https://yourapp.com/.well-known/assetlinks.json` + +3. Update your intent filter with `autoVerify`: + +```xml + + + + + + +``` + +## Related Documentation + +- [Join Session](/calls/react-native/join-session) - Join calls via invite links +- [Events](/calls/react-native/events) - Share invite button events +- [Ringing](/calls/react-native/ringing) - Call notifications diff --git a/calls/react-native/voip-calling.mdx b/calls/react-native/voip-calling.mdx new file mode 100644 index 00000000..3fe65c95 --- /dev/null +++ b/calls/react-native/voip-calling.mdx @@ -0,0 +1,291 @@ +--- +title: "VoIP Calling" +sidebarTitle: "VoIP Calling" +--- + +Implement VoIP push notifications to receive incoming calls even when your app is in the background or terminated. This requires platform-specific configuration for iOS CallKit and Android ConnectionService. + +## iOS VoIP Configuration + +### Enable VoIP Push Notifications + +1. In Xcode, select your target +2. Go to **Signing & Capabilities** +3. Add **Push Notifications** capability +4. Add **Background Modes** capability +5. Enable **Voice over IP** + +### Create VoIP Certificate + +1. Go to [Apple Developer Portal](https://developer.apple.com) +2. Navigate to **Certificates, Identifiers & Profiles** +3. Create a new **VoIP Services Certificate** +4. Download and install the certificate +5. Export the `.p12` file for your server + +### Configure CometChat Dashboard + +1. Go to your CometChat Dashboard +2. Navigate to **Notifications > Push Notifications** +3. Upload your VoIP certificate (`.p12` file) +4. Configure the certificate password + +### Implement CallKit + +Create a native module to handle CallKit: + +```swift +// ios/CallKitManager.swift +import CallKit +import PushKit + +@objc(CallKitManager) +class CallKitManager: NSObject, CXProviderDelegate, PKPushRegistryDelegate { + + static let shared = CallKitManager() + + private let provider: CXProvider + private let callController = CXCallController() + private var voipRegistry: PKPushRegistry? + + override init() { + let config = CXProviderConfiguration() + config.supportsVideo = true + config.maximumCallsPerCallGroup = 1 + config.supportedHandleTypes = [.generic] + + provider = CXProvider(configuration: config) + super.init() + provider.setDelegate(self, queue: nil) + } + + @objc func registerForVoIPPushes() { + voipRegistry = PKPushRegistry(queue: .main) + voipRegistry?.delegate = self + voipRegistry?.desiredPushTypes = [.voIP] + } + + // PKPushRegistryDelegate + func pushRegistry(_ registry: PKPushRegistry, didUpdate pushCredentials: PKPushCredentials, for type: PKPushType) { + let token = pushCredentials.token.map { String(format: "%02x", $0) }.joined() + // Send token to CometChat + NotificationCenter.default.post( + name: NSNotification.Name("VoIPTokenReceived"), + object: nil, + userInfo: ["token": token] + ) + } + + func pushRegistry(_ registry: PKPushRegistry, didReceiveIncomingPushWith payload: PKPushPayload, for type: PKPushType, completion: @escaping () -> Void) { + guard type == .voIP else { return } + + let callerId = payload.dictionaryPayload["callerId"] as? String ?? "Unknown" + let callerName = payload.dictionaryPayload["callerName"] as? String ?? "Unknown" + let sessionId = payload.dictionaryPayload["sessionId"] as? String ?? "" + let hasVideo = payload.dictionaryPayload["hasVideo"] as? Bool ?? false + + reportIncomingCall( + uuid: UUID(), + handle: callerId, + callerName: callerName, + hasVideo: hasVideo + ) { error in + completion() + } + } + + func reportIncomingCall(uuid: UUID, handle: String, callerName: String, hasVideo: Bool, completion: @escaping (Error?) -> Void) { + let update = CXCallUpdate() + update.remoteHandle = CXHandle(type: .generic, value: handle) + update.localizedCallerName = callerName + update.hasVideo = hasVideo + + provider.reportNewIncomingCall(with: uuid, update: update) { error in + completion(error) + } + } + + // CXProviderDelegate + func providerDidReset(_ provider: CXProvider) {} + + func provider(_ provider: CXProvider, perform action: CXAnswerCallAction) { + // Notify React Native to accept the call + NotificationCenter.default.post( + name: NSNotification.Name("CallKitAnswerCall"), + object: nil, + userInfo: ["callUUID": action.callUUID.uuidString] + ) + action.fulfill() + } + + func provider(_ provider: CXProvider, perform action: CXEndCallAction) { + // Notify React Native to end the call + NotificationCenter.default.post( + name: NSNotification.Name("CallKitEndCall"), + object: nil, + userInfo: ["callUUID": action.callUUID.uuidString] + ) + action.fulfill() + } +} +``` + +## Android VoIP Configuration + +### Add Firebase Cloud Messaging + +1. Add Firebase to your Android project +2. Add the FCM dependency to `android/app/build.gradle`: + +```groovy +dependencies { + implementation 'com.google.firebase:firebase-messaging:23.0.0' +} +``` + +### Configure CometChat Dashboard + +1. Go to your CometChat Dashboard +2. Navigate to **Notifications > Push Notifications** +3. Upload your Firebase Server Key + +### Implement ConnectionService + +Create a ConnectionService for incoming calls: + +```java +// android/app/src/main/java/com/yourapp/CallConnectionService.java +package com.yourapp; + +import android.telecom.Connection; +import android.telecom.ConnectionRequest; +import android.telecom.ConnectionService; +import android.telecom.PhoneAccountHandle; +import android.telecom.TelecomManager; + +public class CallConnectionService extends ConnectionService { + + @Override + public Connection onCreateIncomingConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + + CallConnection connection = new CallConnection(); + connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED); + connection.setCallerDisplayName( + request.getExtras().getString("callerName"), + TelecomManager.PRESENTATION_ALLOWED + ); + connection.setRinging(); + + return connection; + } + + @Override + public Connection onCreateOutgoingConnection( + PhoneAccountHandle connectionManagerPhoneAccount, + ConnectionRequest request) { + + CallConnection connection = new CallConnection(); + connection.setConnectionProperties(Connection.PROPERTY_SELF_MANAGED); + connection.setDialing(); + + return connection; + } +} +``` + +### Register ConnectionService + +Add to `AndroidManifest.xml`: + +```xml + + + + + +``` + +### Add Permissions + +```xml + + +``` + +## Register Push Token + +Register the VoIP/FCM token with CometChat: + +```tsx +import { CometChat } from '@cometchat/chat-sdk-react-native'; + +async function registerPushToken(token: string, platform: 'ios' | 'android') { + try { + if (platform === 'ios') { + await CometChat.registerTokenForPushNotification(token, { + voip: true, + }); + } else { + await CometChat.registerTokenForPushNotification(token); + } + console.log('Push token registered'); + } catch (error) { + console.error('Error registering push token:', error); + } +} +``` + +## Handle Incoming VoIP Push + +```tsx +import { useEffect } from 'react'; +import { NativeEventEmitter, NativeModules, Platform } from 'react-native'; +import { CometChat } from '@cometchat/chat-sdk-react-native'; +import { CometChatCalls } from '@cometchat/calls-sdk-react-native'; + +function useVoIPPush() { + useEffect(() => { + if (Platform.OS === 'ios') { + const eventEmitter = new NativeEventEmitter(NativeModules.CallKitManager); + + const answerSubscription = eventEmitter.addListener( + 'CallKitAnswerCall', + async (data) => { + // Accept the call via Chat SDK + const sessionId = data.sessionId; + await CometChat.acceptCall(sessionId); + + // Start the call session + const { token } = await CometChatCalls.generateToken(sessionId); + // Navigate to call screen with token + } + ); + + const endSubscription = eventEmitter.addListener( + 'CallKitEndCall', + async (data) => { + CometChatCalls.leaveSession(); + } + ); + + return () => { + answerSubscription.remove(); + endSubscription.remove(); + }; + } + }, []); +} + +export default useVoIPPush; +``` + +## Related Documentation + +- [Ringing](/calls/react-native/ringing) - Implement call notifications +- [Background Handling](/calls/react-native/background-handling) - Keep calls active in background +- [Setup](/calls/react-native/setup) - Initial SDK setup diff --git a/docs.json b/docs.json index ebda13ce..a5fe4b8c 100644 --- a/docs.json +++ b/docs.json @@ -4826,6 +4826,177 @@ "tab": "SDK", "tab-id": "calls-sdk", "dropdowns": [ + { + "dropdown": "JavaScript", + "icon": "/images/icons/js.svg", + "pages": [ + { + "group": "Overview", + "pages": [ + "calls/javascript/overview" + ] + }, + { + "group": "Integrations", + "pages": [ + "calls/javascript/react-integration", + "calls/javascript/vue-integration", + "calls/javascript/angular-integration", + "calls/javascript/nextjs-integration", + "calls/javascript/ionic-integration" + ] + }, + { + "group": "Getting Started", + "pages": [ + "calls/javascript/setup", + "calls/javascript/authentication" + ] + }, + { + "group": "Call Session", + "pages": [ + "calls/javascript/session-settings", + "calls/javascript/join-session", + "calls/javascript/actions", + "calls/javascript/events" + ] + }, + { + "group": "Features", + "pages": [ + "calls/javascript/ringing", + "calls/javascript/call-layouts", + "calls/javascript/recording", + "calls/javascript/call-logs", + "calls/javascript/participant-management", + "calls/javascript/screen-sharing", + "calls/javascript/virtual-background", + "calls/javascript/picture-in-picture", + "calls/javascript/raise-hand", + "calls/javascript/idle-timeout" + ] + }, + { + "group": "Advanced", + "pages": [ + "calls/javascript/custom-control-panel", + "calls/javascript/device-management", + "calls/javascript/permissions-handling", + "calls/javascript/in-call-chat", + "calls/javascript/share-invite" + ] + } + ] + }, + { + "dropdown": "React Native", + "icon": "/images/icons/react.svg", + "pages": [ + { + "group": "Overview", + "pages": [ + "calls/react-native/overview" + ] + }, + { + "group": "Getting Started", + "pages": [ + "calls/react-native/setup", + "calls/react-native/authentication", + "calls/react-native/session-settings" + ] + }, + { + "group": "Call Session", + "pages": [ + "calls/react-native/join-session", + "calls/react-native/actions", + "calls/react-native/events" + ] + }, + { + "group": "Features", + "pages": [ + "calls/react-native/call-layouts", + "calls/react-native/call-logs", + "calls/react-native/recording", + "calls/react-native/participant-management", + "calls/react-native/screen-sharing", + "calls/react-native/audio-modes", + "calls/react-native/raise-hand", + "calls/react-native/idle-timeout", + "calls/react-native/ringing" + ] + }, + { + "group": "Advanced", + "pages": [ + "calls/react-native/picture-in-picture", + "calls/react-native/voip-calling", + "calls/react-native/background-handling", + "calls/react-native/custom-control-panel", + "calls/react-native/custom-participant-list", + "calls/react-native/in-call-chat", + "calls/react-native/share-invite" + ] + } + ] + }, + { + "dropdown": "iOS", + "icon": "/images/icons/swift.svg", + "pages": [ + { + "group": "Overview", + "pages": [ + "calls/ios/overview" + ] + }, + { + "group": "Getting Started", + "pages": [ + "calls/ios/setup", + "calls/ios/authentication" + ] + }, + { + "group": "Call Session", + "pages": [ + "calls/ios/session-settings", + "calls/ios/join-session", + "calls/ios/actions", + "calls/ios/events" + ] + }, + { + "group": "Features", + "pages": [ + "calls/ios/ringing", + "calls/ios/call-layouts", + "calls/ios/audio-modes", + "calls/ios/recording", + "calls/ios/call-logs", + "calls/ios/participant-management", + "calls/ios/screen-sharing", + "calls/ios/picture-in-picture", + "calls/ios/raise-hand", + "calls/ios/idle-timeout" + ] + }, + { + "group": "Advanced", + "pages": [ + "calls/ios/custom-control-panel", + "calls/ios/custom-participant-list", + "calls/ios/voip-calling", + "calls/ios/background-handling", + "calls/ios/in-call-chat", + "calls/ios/share-invite" + ] + } + ] + }, { "dropdown": "Android", "icon": "/images/icons/android.svg", @@ -4880,40 +5051,12 @@ } ] }, - { - "dropdown": "iOS", - "icon": "/images/icons/swift.svg", - "pages": [ - "calls/ios/overview" - ] - }, - { - "dropdown": "React Native", - "icon": "/images/icons/react.svg", - "pages": [ - "calls/react-native/overview" - ] - }, - { - "dropdown": "JavaScript", - "icon": "/images/icons/js.svg", - "pages": [ - "calls/javascript/overview" - ] - }, { "dropdown": "Flutter", "icon": "/images/icons/flutter.svg", "pages": [ "calls/flutter/overview" ] - }, - { - "dropdown": "Ionic", - "icon": "/images/icons/ionic.svg", - "pages": [ - "calls/ionic/overview" - ] } ] }, From e77d4ee87d7d60bf148a915f1381de6f19111d67 Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Fri, 16 Jan 2026 15:55:16 +0530 Subject: [PATCH 07/10] docs(calls): restructure and enhance Calls documentation with improved hero section and iOS setup - Update calls.mdx with clearer description focusing on WebRTC-powered SDKs - Redesign hero section with improved layout, responsive columns, and banner image - Add "What is CometChat Calls?" section with feature highlights and callout - Add "How It Works" section with sequence diagram for call flow - Restructure iOS documentation (overview.mdx and setup.mdx) for better clarity - Add .kiro to .gitignore for build artifacts - Improve visual hierarchy and dark mode support throughout - Enhance documentation with better explanations of SDK capabilities and use cases --- .gitignore | 1 + calls.mdx | 478 ++++++++++++++++++----------------------- calls/ios/overview.mdx | 2 +- calls/ios/setup.mdx | 2 +- 4 files changed, 216 insertions(+), 267 deletions(-) diff --git a/.gitignore b/.gitignore index e6aff516..16f9140a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store +.kiro diff --git a/calls.mdx b/calls.mdx index eea8d3b5..06e13b5a 100644 --- a/calls.mdx +++ b/calls.mdx @@ -1,311 +1,259 @@ --- mode: "custom" title: "Voice & Video Calling" -description: "Add real-time chat, voice & video calling to your apps in minutes. Choose your path: UI Kits, SDKs, or Widgets." +description: "Add real-time voice and video calling to your apps in minutes with WebRTC-powered SDKs." canonical: "https://cometchat.com/docs" --- import { CardGroup, Card, Icon, Badge, Steps, Columns, AccordionGroup, Accordion, Callout } from 'mintlify'; {/* Hero Section */} -
-
+
+
+ +
+ +
+

Voice & Video Calling

+

+ Add high-quality, real-time voice and video calls to any app. Built on WebRTC with drop-in UI components. +

+
+
+ Voice & Video Calling UI Preview +
+
+
+
+ +
+ +{/* What is CometChat Calls */} +
+

What is CometChat Calls?

+ +

+ The CometChat Calls SDK provides a complete voice and video calling solution built on WebRTC. It includes: +

-
- -
-

Voice & Video Calling

-

- Integrate high-quality voice and video calling into your app with ease. -

+
+
+ +
+ Built-in UI +

Pre-built call screens, controls, and participant views

+
-
- Voice & Video Calling UI Preview +
+ +
+ Rich Call Controls +

Mute, screen share, recording, layouts, and more

+
- -
+
+ +
+ Cross-Platform +

Web, iOS, Android, React Native, and Flutter

+
+
+
+ +
+ Scalable Infrastructure +

Enterprise-grade media servers handle all complexity

+
+
+
+ + **Already using CometChat UI Kits?** Voice and video calling is already integrated with ready-to-use components. Use the Calls SDK directly only if you need custom call UI or advanced control. +
-
- -

- Step 1 -

- +{/* How It Works */}
-

Add CometChat to Your Frontend

-

Use our pre-built SDKs to add calls and Voice to your website or mobile app instantly.

+

How It Works

+ + ```mermaid + sequenceDiagram + participant App + participant CometChatCalls + participant MediaServer + + App->>CometChatCalls: 1. init(appId, region) + App->>CometChatCalls: 2. login(authToken) + App->>CometChatCalls: 3. generateToken(sessionId) + App->>CometChatCalls: 4. joinSession(token, settings) + CometChatCalls->>MediaServer: Connect to call + MediaServer-->>CometChatCalls: Media streams + CometChatCalls-->>App: Session joined + UI rendered + App->>CometChatCalls: 5. Actions (mute, layout, etc.) + CometChatCalls-->>App: Event callbacks + App->>CometChatCalls: 6. leaveSession() + ```
-{/* SDKs Section */} +{/* Choose Your Platform */}
-

- SDKs (Includes UI) -

- -

- Access every calling capability with our SDKs. The Calls SDK ships with a drop‑in UI so you can go live fast. -

- - - - - - - Initialize and connect to CometChat in your frontend application. - - - Create UI and flows from the ground up—exactly how you want them. - - - Full control over every aspect of the chat experience. - - - - - - - Live in minutes, not days. - No framework lock‑in or build steps. - Configure without code; theme when needed. - - - - - - - } href="/calls/javascript/overview" horizontal /> - } href="/calls/react-native/overview" horizontal /> - } href="/calls/ios/overview" horizontal /> - } href="/calls/android/overview" horizontal /> - } href="/calls/flutter/overview" horizontal /> +

Choose Your Platform

+ + + } href="/calls/javascript/overview"> + Web apps with React, Vue, Angular, Next.js, or vanilla JS + + } href="/calls/react-native/overview"> + Cross-platform mobile apps + + } href="/calls/ios/overview"> + Native Swift/Objective-C apps + + } href="/calls/android/overview"> + Native Kotlin/Java apps + + } href="/calls/flutter/overview"> + Cross-platform with Dart + + + Server-side call management +
-

- Step 2 -

- -
-

- Sync Your Users -

- -

- Sync your user database with CometChat for a seamless experience. -

- - - -
    -
  • Add users directly from the CometChat Dashboard.
  • -
  • Ideal for quick testing or small teams.
  • -
-
- - -
    -
  • Create users via the SDK methods.
  • -
  • Perfect for auto-provisioning during sign-up or login.
  • -
-
- - -
    -
  • Create users using the REST API.
  • -
  • Best for batch imports or admin workflows.
  • -
-
-
- -{/*

- Verify the Integration -

- - - - - Log in as **two test users** and exchange a few messages. - - Confirm **delivery** and **read** states render correctly. - - - - - Post in a **test group** and verify messages reach **all members**. - - Check **join/leave** events (if enabled) appear as expected. - - - - - Ensure **typing indicators** show while the other user composes. - - Verify **read receipts** update after viewing. - - Confirm **presence** toggles between **Online / Offline**. - - - - - **Refresh the browser**; the session should recover gracefully. - - Toggle **network offline/online** briefly; messages should re-sync. - - Validate **error toasts/logging** for any failed sends. - - - - - All steps passing? You’re ready for staging or production. - */} +{/* Key Features */} +
+

Key Features

+ + + + Tile, Sidebar, and Spotlight views + + + Record sessions with cloud storage + + + Share screens with participants + + + Mute, pin, kick participants + + + Incoming/outgoing notifications + + + Call history and analytics + + + Blur or custom backgrounds (Web) + + + Multitask during calls (JS, Android) + + + Signal to get attention during calls + + + Speaker, earpiece, Bluetooth (Mobile) + + + Customize call UI controls + + + Auto-end calls when alone + + + Select camera/mic devices (Web) + + +
+{/* Platform Comparison */} +
+

Platform Feature Comparison

+ +
+ | Feature | JavaScript | React Native | iOS | Android | Flutter | + |---------|:----------:|:------------:|:---:|:-------:|:-------:| + | Voice Calls | ✅ | ✅ | ✅ | ✅ | ✅ | + | Video Calls | ✅ | ✅ | ✅ | ✅ | ✅ | + | Screen Sharing | ✅ | ✅ | ✅ | ✅ | ✅ | + | Recording | ✅ | ✅ | ✅ | ✅ | ✅ | + | Virtual Background | ✅ | ❌ | ❌ | ❌ | ❌ | + | Picture-in-Picture | ✅ | ❌ | ❌ | ✅ | ❌ | + | Raise Hand | ✅ | ✅ | ✅ | ✅ | ✅ | + | Audio Modes | ❌ | ✅ | ✅ | ✅ | ✅ | + | Custom Control Panel | ✅ | ✅ | ✅ | ✅ | ✅ | + | Idle Timeout | ✅ | ✅ | ✅ | ✅ | ✅ | + | Device Management | ✅ | ❌ | ❌ | ❌ | ❌ | +
-{/* Sample Apps Section */} +{/* Browser & Platform Compatibility */}
-

- Sample Apps & Demos -

+

Browser & Platform Compatibility

- See CometChat in action. Clone these sample apps to get started quickly. + The Calls SDK requires WebRTC support. HTTPS is required for camera/microphone access in production (localhost is exempt during development).

- - - - - - - - - - +
+ | Platform | Browser / Framework | Minimum Version | + |----------|---------------------|-----------------| + | **Desktop** | Chrome | 72+ | + | | Firefox | 68+ | + | | Safari | 12.1+ | + | | Edge (Chromium) | 79+ | + | | Opera | 60+ | + | **Mobile Web** | Chrome for Android | Full support | + | | Safari for iOS | iOS 12.1+ | + | | Firefox for Android | Full support | + | | Samsung Internet | 12+ | + | **Native Mobile** | iOS | 16.0+ | + | | Android | API 24 (Android 7.0+) | + | **Cross-Platform** | React Native | 0.70+ | + | | Flutter | 3.0+ | +
-{/* Resources Section */} -{/*
-

- Help & Resources -

+{/* Sync Your Users */} +
+

Sync Your Users

- Get help, explore demos, and stay updated with the latest features. + Users must exist in CometChat before they can make or receive calls.

- - - Search our knowledge base for answers to all your questions. + + + Add users manually for quick testing - - Experience CometChat in action with our live demo. + + Create users during sign-up/login flows - - Get notified about new features and improvements. - - - Monitor service status and any ongoing issues. + + Batch imports or backend workflows -
*/} +
-{/* Office Hours */} -{/*
- - - Get personalized guidance from our solution engineers to optimize your integration. +{/* Resources */} +
+

Resources

+ + + + Server-side APIs for call management, recordings, and analytics + + + Troubleshooting guides and FAQs -
*/} -
-{/* Footer */} - diff --git a/calls/ios/overview.mdx b/calls/ios/overview.mdx index 4195ffa0..59a4ceba 100644 --- a/calls/ios/overview.mdx +++ b/calls/ios/overview.mdx @@ -25,7 +25,7 @@ Before integrating the Calls SDK, ensure you have: 1. **CometChat Account**: [Sign up](https://app.cometchat.com/signup) and create an app to get your App ID, Region, and API Key 2. **CometChat Users**: Users must exist in CometChat to use calling features. For testing, create users via the [Dashboard](https://app.cometchat.com) or [REST API](/rest-api/chat-apis/users/create-user). Authentication is handled by the Calls SDK - see [Authentication](/calls/ios/authentication) 3. **iOS Requirements**: - - Minimum iOS version: 13.0 + - Minimum iOS version: 16.0 - Xcode 14.0 or later - Swift 5.0 or later 4. **Permissions**: Camera and microphone permissions for video/audio calls diff --git a/calls/ios/setup.mdx b/calls/ios/setup.mdx index e66d52dd..026a715e 100644 --- a/calls/ios/setup.mdx +++ b/calls/ios/setup.mdx @@ -12,7 +12,7 @@ This guide walks you through installing the CometChat Calls SDK and initializing Add the CometChat Calls SDK to your `Podfile`: ```ruby -platform :ios, '13.0' +platform :ios, '16.0' use_frameworks! target 'YourApp' do From 5dc4cf700be6a7d37443c356337045f612f87616 Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Fri, 16 Jan 2026 23:04:44 +0530 Subject: [PATCH 08/10] docs(calls): restructure Calls documentation and add platform-specific guides - Add new platform documentation section with compatibility matrix, features overview, and user sync guides - Restructure Calls landing page with simplified hero section and improved content organization - Add calling integration guides for all UI Kit platforms (Android, iOS, React, React Native, Flutter) - Update version aligner CSS to support flexible width badges with better responsive behavior - Enhance platform overview pages for Android, iOS, JavaScript, and React Native with consistent structure - Update docs.json to reflect new documentation hierarchy and platform guides - Consolidate cross-platform calling patterns and best practices in dedicated platform section --- assets/version-aligner.css | 7 +- calls.mdx | 374 +++++++----------- calls/android/overview.mdx | 4 +- calls/ios/overview.mdx | 4 +- calls/javascript/overview.mdx | 4 +- calls/platform/compatibility.mdx | 162 ++++++++ calls/platform/features.mdx | 402 ++++++++++++++++++++ calls/platform/overview.mdx | 72 ++++ calls/platform/user-sync.mdx | 262 +++++++++++++ calls/react-native/overview.mdx | 4 +- docs.json | 35 +- ui-kit/android/call-features.mdx | 96 +---- ui-kit/android/calling-integration.mdx | 102 +++++ ui-kit/flutter/call-features.mdx | 196 +--------- ui-kit/flutter/calling-integration.mdx | 145 +++++++ ui-kit/ios/call-features.mdx | 88 +---- ui-kit/ios/calling-integration.mdx | 86 +++++ ui-kit/react-native/call-features.mdx | 218 +---------- ui-kit/react-native/calling-integration.mdx | 157 ++++++++ ui-kit/react/call-features.mdx | 20 +- ui-kit/react/calling-integration.mdx | 28 ++ 21 files changed, 1614 insertions(+), 852 deletions(-) create mode 100644 calls/platform/compatibility.mdx create mode 100644 calls/platform/features.mdx create mode 100644 calls/platform/overview.mdx create mode 100644 calls/platform/user-sync.mdx create mode 100644 ui-kit/android/calling-integration.mdx create mode 100644 ui-kit/flutter/calling-integration.mdx create mode 100644 ui-kit/ios/calling-integration.mdx create mode 100644 ui-kit/react-native/calling-integration.mdx create mode 100644 ui-kit/react/calling-integration.mdx diff --git a/assets/version-aligner.css b/assets/version-aligner.css index 837fd218..c022ff23 100644 --- a/assets/version-aligner.css +++ b/assets/version-aligner.css @@ -51,9 +51,10 @@ html.cc-version-aligned #sidebar-content .cc-version-aligned-row [data-version-a border-radius: 999px !important; border: 1px solid rgba(15, 23, 42, 0.15); transition: border-color 0.2s ease, background-color 0.2s ease; - width: 10rem; - max-width: 10rem; - flex: 0 0 10rem; + width: auto; + min-width: 10rem; + max-width: 14rem; + flex: 0 0 auto; overflow: visible; } diff --git a/calls.mdx b/calls.mdx index 06e13b5a..76ed9a75 100644 --- a/calls.mdx +++ b/calls.mdx @@ -5,255 +5,169 @@ description: "Add real-time voice and video calling to your apps in minutes with canonical: "https://cometchat.com/docs" --- -import { CardGroup, Card, Icon, Badge, Steps, Columns, AccordionGroup, Accordion, Callout } from 'mintlify'; +import { CardGroup, Card, Steps, Columns, AccordionGroup, Accordion, Callout } from 'mintlify'; {/* Hero Section */} -
-
- -
- -
-

Voice & Video Calling

-

- Add high-quality, real-time voice and video calls to any app. Built on WebRTC with drop-in UI components. -

-
-
- Voice & Video Calling UI Preview -
-
-
-
- -
- -{/* What is CometChat Calls */} -
-

What is CometChat Calls?

- -

- The CometChat Calls SDK provides a complete voice and video calling solution built on WebRTC. It includes: -

- -
-
- -
- Built-in UI -

Pre-built call screens, controls, and participant views

-
+
+
+ +
+ +
+

Voice & Video Calling

+

+ Add high-quality, real-time voice and video calls to any app. Built on WebRTC with drop-in UI components. +

-
- -
- Rich Call Controls -

Mute, screen share, recording, layouts, and more

-
+
+ Voice & Video Calling UI Preview
-
- -
- Cross-Platform -

Web, iOS, Android, React Native, and Flutter

-
-
-
- -
- Scalable Infrastructure -

Enterprise-grade media servers handle all complexity

-
-
-
- - - **Already using CometChat UI Kits?** Voice and video calling is already integrated with ready-to-use components. Use the Calls SDK directly only if you need custom call UI or advanced control. - +
-{/* How It Works */} -
-

How It Works

- - ```mermaid - sequenceDiagram - participant App - participant CometChatCalls - participant MediaServer - - App->>CometChatCalls: 1. init(appId, region) - App->>CometChatCalls: 2. login(authToken) - App->>CometChatCalls: 3. generateToken(sessionId) - App->>CometChatCalls: 4. joinSession(token, settings) - CometChatCalls->>MediaServer: Connect to call - MediaServer-->>CometChatCalls: Media streams - CometChatCalls-->>App: Session joined + UI rendered - App->>CometChatCalls: 5. Actions (mute, layout, etc.) - CometChatCalls-->>App: Event callbacks - App->>CometChatCalls: 6. leaveSession() - ```
-{/* Choose Your Platform */} -
-

Choose Your Platform

- - - } href="/calls/javascript/overview"> - Web apps with React, Vue, Angular, Next.js, or vanilla JS - - } href="/calls/react-native/overview"> - Cross-platform mobile apps - - } href="/calls/ios/overview"> - Native Swift/Objective-C apps - - } href="/calls/android/overview"> - Native Kotlin/Java apps - - } href="/calls/flutter/overview"> - Cross-platform with Dart - - - Server-side call management - - -
- -{/* Key Features */} -
-

Key Features

- - - - Tile, Sidebar, and Spotlight views - - - Record sessions with cloud storage - - - Share screens with participants - - - Mute, pin, kick participants - - - Incoming/outgoing notifications - - - Call history and analytics - - - Blur or custom backgrounds (Web) - - - Multitask during calls (JS, Android) - - - Signal to get attention during calls - - - Speaker, earpiece, Bluetooth (Mobile) - - - Customize call UI controls - - - Auto-end calls when alone - - - Select camera/mic devices (Web) - - -
+
-{/* Platform Comparison */}
-

Platform Feature Comparison

- -
- | Feature | JavaScript | React Native | iOS | Android | Flutter | - |---------|:----------:|:------------:|:---:|:-------:|:-------:| - | Voice Calls | ✅ | ✅ | ✅ | ✅ | ✅ | - | Video Calls | ✅ | ✅ | ✅ | ✅ | ✅ | - | Screen Sharing | ✅ | ✅ | ✅ | ✅ | ✅ | - | Recording | ✅ | ✅ | ✅ | ✅ | ✅ | - | Virtual Background | ✅ | ❌ | ❌ | ❌ | ❌ | - | Picture-in-Picture | ✅ | ❌ | ❌ | ✅ | ❌ | - | Raise Hand | ✅ | ✅ | ✅ | ✅ | ✅ | - | Audio Modes | ❌ | ✅ | ✅ | ✅ | ✅ | - | Custom Control Panel | ✅ | ✅ | ✅ | ✅ | ✅ | - | Idle Timeout | ✅ | ✅ | ✅ | ✅ | ✅ | - | Device Management | ✅ | ❌ | ❌ | ❌ | ❌ | +

What is CometChat Calls?

+

The CometChat Calls SDK provides a complete voice and video calling solution built on WebRTC.

+ + + + + + Pre-built call screens, controls, and participant views + + + Mute, screen share, recording, layouts, and more + + + Web, iOS, Android, React Native, and Flutter + + + Enterprise-grade media servers handle all complexity + + + + +**Already using CometChat UI Kits?** Voice and video calling is already integrated with ready-to-use components. Use the Calls SDK directly only if you need custom call UI or advanced control. +
-{/* Browser & Platform Compatibility */}
-

Browser & Platform Compatibility

- -

- The Calls SDK requires WebRTC support. HTTPS is required for camera/microphone access in production (localhost is exempt during development). -

- -
- | Platform | Browser / Framework | Minimum Version | - |----------|---------------------|-----------------| - | **Desktop** | Chrome | 72+ | - | | Firefox | 68+ | - | | Safari | 12.1+ | - | | Edge (Chromium) | 79+ | - | | Opera | 60+ | - | **Mobile Web** | Chrome for Android | Full support | - | | Safari for iOS | iOS 12.1+ | - | | Firefox for Android | Full support | - | | Samsung Internet | 12+ | - | **Native Mobile** | iOS | 16.0+ | - | | Android | API 24 (Android 7.0+) | - | **Cross-Platform** | React Native | 0.70+ | - | | Flutter | 3.0+ | -
+

Choose Your Platform

+

Get started with the Calls SDK on your preferred platform. Each SDK provides the same core calling features with platform-specific optimizations.

+ + + + + + Add the Calls SDK to your project via npm, CocoaPods, Gradle, or pub. + + + Configure with your App ID and Region, then log in users with their CometChat credentials. + + + Generate a session token and join—the SDK renders a complete call UI automatically. + + + Adjust layouts, controls, and styling to match your app's design. + + + + + + Pre-built call screens, controls, and participant views—no UI work required. + Enterprise-grade media infrastructure handles all the complexity. + Same features and API patterns across all platforms. + Get up and running in minutes with simple setup and clear documentation. + + + + + + } href="/calls/javascript/overview"> + Vanilla JS or any web framework + + } href="/calls/javascript/react-integration"> + React web applications + + } href="/calls/javascript/angular-integration"> + Angular web applications + + } href="/calls/javascript/vue-integration"> + Vue.js web applications + + } href="/calls/javascript/nextjs-integration"> + Next.js with SSR support + + } href="/calls/javascript/ionic-integration"> + Ionic hybrid mobile apps + + } href="/calls/react-native/overview"> + Cross-platform mobile apps + + } href="/calls/ios/overview"> + Native Swift/Objective-C apps + + } href="/calls/android/overview"> + Native Kotlin/Java apps + + } href="/calls/flutter/overview"> + Cross-platform with Dart + +
-{/* Sync Your Users */}
-

Sync Your Users

- -

- Users must exist in CometChat before they can make or receive calls. -

- - - - Add users manually for quick testing - - - Create users during sign-up/login flows - - - Batch imports or backend workflows - - +

Sample Apps

+

Explore our open-source sample apps to see the Calls SDK in action. Clone, run, and customize for your needs.

+ + + + Web calling with React, Vue, and vanilla JS + + + Native Android with Kotlin/Java + + + Native iOS with Swift + + + Cross-platform mobile app + + + Cross-platform with Dart + +
-{/* Resources */} -
-

Resources

- - - - Server-side APIs for call management, recordings, and analytics - - - Troubleshooting guides and FAQs - - +
+

Resources

+ + + + Features, compatibility, and platform comparison + + + Server-side APIs for call management and recordings + + + Troubleshooting guides and FAQs + +
diff --git a/calls/android/overview.mdx b/calls/android/overview.mdx index a1c0bb07..395688a8 100644 --- a/calls/android/overview.mdx +++ b/calls/android/overview.mdx @@ -8,12 +8,12 @@ The CometChat Calls SDK enables real-time voice and video calling capabilities i **Faster Integration with UI Kits** -If you're using CometChat UI Kits, voice and video calling is already integrated with ready-to-use components: +If you're using CometChat UI Kits, voice and video calling can be quickly integrated: - Incoming & outgoing call screens - Call buttons with one-tap calling - Call logs with history -👉 [Android UI Kit Call Features](/ui-kit/android/call-features) +👉 [Android UI Kit Calling Integration](/ui-kit/android/calling-integration) Use this Calls SDK directly only if you need custom call UI or advanced control. diff --git a/calls/ios/overview.mdx b/calls/ios/overview.mdx index 59a4ceba..9436784c 100644 --- a/calls/ios/overview.mdx +++ b/calls/ios/overview.mdx @@ -8,12 +8,12 @@ The CometChat Calls SDK enables real-time voice and video calling capabilities i **Faster Integration with UI Kits** -If you're using CometChat UI Kits, voice and video calling is already integrated with ready-to-use components: +If you're using CometChat UI Kits, voice and video calling can be quickly integrated: - Incoming & outgoing call screens - Call buttons with one-tap calling - Call logs with history -👉 [iOS UI Kit Call Features](/ui-kit/ios/call-features) +👉 [iOS UI Kit Calling Integration](/ui-kit/ios/calling-integration) Use this Calls SDK directly only if you need custom call UI or advanced control. diff --git a/calls/javascript/overview.mdx b/calls/javascript/overview.mdx index 6b456f17..7c381e4b 100644 --- a/calls/javascript/overview.mdx +++ b/calls/javascript/overview.mdx @@ -8,12 +8,12 @@ The CometChat Calls SDK enables real-time voice and video calling capabilities i **Faster Integration with UI Kits** -If you're using CometChat UI Kits, voice and video calling is already integrated with ready-to-use components: +If you're using CometChat UI Kits, voice and video calling can be quickly integrated: - Incoming & outgoing call screens - Call buttons with one-tap calling - Call logs with history -👉 [React UI Kit Call Features](/ui-kit/react/call-features) +👉 [React UI Kit Calling Integration](/ui-kit/react/calling-integration) Use this Calls SDK directly only if you need custom call UI or advanced control. diff --git a/calls/platform/compatibility.mdx b/calls/platform/compatibility.mdx new file mode 100644 index 00000000..64b75573 --- /dev/null +++ b/calls/platform/compatibility.mdx @@ -0,0 +1,162 @@ +--- +title: "Platform Compatibility" +sidebarTitle: "Compatibility" +description: "Feature availability, browser support, and platform requirements for CometChat Calls SDK." +--- + +import { Callout, CardGroup, Card } from 'mintlify'; + +This page provides detailed information about feature availability across platforms, browser support, and minimum version requirements. + +## Platform Feature Comparison + +Not all features are available on every platform. Use this table to understand feature availability: + +| Feature | JavaScript | React Native | iOS | Android | Flutter | +|---------|:----------:|:------------:|:---:|:-------:|:-------:| +| **Core Features** | | | | | | +| Voice Calls | ✓ | ✓ | ✓ | ✓ | ✓ | +| Video Calls | ✓ | ✓ | ✓ | ✓ | ✓ | +| Group Calls | ✓ | ✓ | ✓ | ✓ | ✓ | +| Screen Sharing | ✓ | ✓ | ✓ | ✓ | ✓ | +| Recording | ✓ | ✓ | ✓ | ✓ | ✓ | +| Call Layouts | ✓ | ✓ | ✓ | ✓ | ✓ | +| **Call Management** | | | | | | +| Ringing | ✓ | ✓ | ✓ | ✓ | ✓ | +| Call Logs | ✓ | ✓ | ✓ | ✓ | ✓ | +| Participant Management | ✓ | ✓ | ✓ | ✓ | ✓ | +| In-Call Chat | ✓ | ✓ | ✓ | ✓ | ✓ | +| **Advanced Features** | | | | | | +| Virtual Background | ✓ | ✗ | ✗ | ✗ | ✗ | +| Picture-in-Picture | ✓ | ✗ | ✗ | ✓ | ✗ | +| Raise Hand | ✓ | ✓ | ✓ | ✓ | ✓ | +| Audio Modes | ✗ | ✓ | ✓ | ✓ | ✓ | +| **Customization** | | | | | | +| Custom Control Panel | ✓ | ✓ | ✓ | ✓ | ✓ | +| Idle Timeout | ✓ | ✓ | ✓ | ✓ | ✓ | +| Device Management | ✓ | ✗ | ✗ | ✗ | ✗ | +| **Platform-Specific** | | | | | | +| VoIP / CallKit | ✗ | ✓ | ✓ | ✓ | ✗ | +| Background Handling | ✗ | ✓ | ✓ | ✓ | ✗ | + +✓ = Available, ✗ = Not Available + +## Browser Support + +### Desktop Browsers + +| Browser | Minimum Version | Notes | +|---------|-----------------|-------| +| Chrome | 72+ | Recommended | +| Firefox | 68+ | Full support | +| Safari | 12.1+ | macOS 10.14.4+ | +| Edge | 79+ | Chromium-based | +| Opera | 60+ | Chromium-based | + +### Mobile Browsers + +| Browser | Minimum Version | +|---------|-----------------| +| Chrome for Android | 72+ | +| Firefox for Android | 68+ | +| Samsung Internet | 12+ | +| Edge for Android | 79+ | +| Safari for iOS | 12.1+ | +| Chrome for iOS | 72+ | +| Edge for iOS | 79+ | + +## Native Mobile Platforms + +### iOS + +| Requirement | Value | +|-------------|-------| +| Minimum Version | iOS 16.0+ | +| Architecture | arm64 | +| Package Manager | CocoaPods, SPM | +| Platform Capabilities | CallKit, VoIP Push, Background Audio | + +### Android + +| Requirement | Value | +|-------------|-------| +| Minimum API | API 24 (Android 7.0+) | +| Architecture | arm64-v8a, armeabi-v7a | +| Package Manager | Gradle | +| Platform Capabilities | PiP Mode, Foreground Service, ConnectionService | + +## Cross-Platform Frameworks + +### React Native + +| Requirement | Value | +|-------------|-------| +| Minimum Version | 0.70+ | +| Platforms | iOS, Android | +| Package Manager | npm, yarn | +| Expo Support | Bare workflow | + +### Flutter + +| Requirement | Value | +|-------------|-------| +| Minimum Flutter | 3.0+ | +| Minimum Dart | 2.17+ | +| Platforms | iOS, Android | +| Package Manager | pub | + +## Network Requirements + +### Ports & Protocols + +| Protocol | Port | Purpose | +|----------|------|---------| +| HTTPS | 443 | API calls, signaling | +| UDP | 10000-20000 | Media streaming (preferred) | +| TCP | 443 | Media fallback (TURN) | + +### Bandwidth Recommendations + +| Call Type | Bandwidth | +|-----------|-----------| +| Voice Call | 100 - 300 kbps | +| Video Call (SD) | 500 kbps - 1 Mbps | +| Video Call (HD) | 1.5 - 3 Mbps | +| Screen Sharing | 1 - 2 Mbps | + + +**HTTPS Required**: Camera and microphone access requires HTTPS in production environments. Localhost (`http://localhost` or `http://127.0.0.1`) is exempt during development. + + +## Permissions Required + +### Web +- Camera access +- Microphone access +- Screen capture (for screen sharing) +- Notifications (optional) + +### iOS +``` +NSCameraUsageDescription +NSMicrophoneUsageDescription +``` + +### Android +```xml + + + + +``` + +## Next Steps + + + + Explore all available features + + + Set up user authentication + + diff --git a/calls/platform/features.mdx b/calls/platform/features.mdx new file mode 100644 index 00000000..3267146f --- /dev/null +++ b/calls/platform/features.mdx @@ -0,0 +1,402 @@ +--- +title: "Key Features" +sidebarTitle: "Key Features" +description: "Comprehensive guide to all CometChat Calls SDK features and capabilities." +--- + +import { Callout, CardGroup, Card } from 'mintlify'; + +The Calls SDK includes a comprehensive set of features for building professional calling experiences. Each feature is designed to work seamlessly across platforms while providing the flexibility to customize for your specific use case. + +## Core Calling Features + +### Voice Calls + +High-quality audio calls powered by WebRTC with enterprise-grade infrastructure designed for reliability and clarity. + +**How It Works:** +Voice calls use WebRTC's audio processing pipeline with automatic codec selection (Opus preferred) for optimal quality. The SDK handles all the complexity of establishing peer connections, managing ICE candidates, and maintaining stable audio streams. + +**Technical Capabilities:** +- Adaptive bitrate encoding that adjusts to network conditions +- Automatic noise cancellation and echo reduction +- Automatic gain control for consistent volume levels +- Low latency audio streaming optimized for real-time communication +- Automatic reconnection on network interruptions + +**Call Types Supported:** +- 1-to-1 private voice calls +- Group voice calls with multiple participants +- Audio-only mode in video calls + +**Use Cases:** Customer support calls, team meetings, social audio rooms, telehealth consultations + +--- + +### Video Calls + +HD video calling with adaptive quality optimization that delivers smooth video across varying network conditions. + +**How It Works:** +Video calls leverage WebRTC's video processing with VP8/VP9/H.264 codec support. The SDK automatically selects the best codec based on browser/device capabilities and negotiates optimal resolution and framerate. + +**Technical Capabilities:** +- HD video support with adaptive bitrate +- Automatic quality adjustment based on network conditions +- Front/back camera switching on mobile devices +- Camera on/off toggle during calls +- Video preview before joining calls + +**Use Cases:** Video consultations, remote interviews, virtual events, family video calls, remote team collaboration + +--- + +### Group Calls + +Multi-participant calling with intelligent layout management and active speaker detection. + +**How It Works:** +Group calls use a Selective Forwarding Unit (SFU) architecture where media streams are routed through CometChat's servers. This enables efficient bandwidth usage as each participant only uploads one stream while receiving multiple streams. + +**Participant Features:** +- Support for multiple simultaneous video/audio participants +- Dynamic participant join/leave handling +- Participant list with real-time status (muted, video off) +- Active speaker detection and highlighting + +**Use Cases:** Team meetings, webinars, virtual classrooms, group therapy sessions, online workshops + +--- + +### Call Layouts + +Three view modes that automatically adapt to participant count and call context. + +**Available Layouts:** + +| Layout | Description | Best For | +|--------|-------------|----------| +| **Tile** | Grid layout with equally-sized tiles | Group discussions, team meetings | +| **Sidebar** | Main speaker with participants in a sidebar | Presentations, webinars | +| **Spotlight** | Large view for active speaker, small tiles for others | One-on-one calls, focused discussions | + +**Features:** +- Set initial layout when joining a session +- Switch layouts dynamically during calls +- Each participant can independently choose their preferred layout +- Automatic layout optimization based on participant count + +--- + +### Screen Sharing + +Share your screen with other participants during a call. + +**How It Works:** +Screen sharing uses the browser's `getDisplayMedia` API (web) or native screen capture APIs (mobile). The shared content is encoded as a video stream and transmitted to other participants. + +**Sharing Options (Web):** +- Entire screen +- Application window +- Browser tab + +**Features:** +- Start/stop screen sharing programmatically +- Screen share events for local and remote participants +- Configurable screen share button visibility +- Button click interception for custom behavior + +**Use Cases:** Software demos, slide presentations, collaborative document editing, remote tech support, code reviews + +--- + +### Recording + +Server-side call recording with cloud storage for later playback and compliance. + +**How It Works:** +Recording happens on CometChat's media servers, not on participant devices. This ensures consistent quality regardless of individual network conditions and eliminates the risk of lost recordings due to client crashes. + + +Recording must be enabled for your CometChat app. Contact support if you need to enable this feature. + + +**Features:** +- Auto-start recording when session begins (configurable) +- Manual start/stop recording during calls +- Recording status indicators for all participants +- Recording events (started, stopped) +- Access recordings via CometChat Dashboard or REST API + +**Recording Controls:** +- `startRecording()` - Begin recording +- `stopRecording()` - End recording +- `autoStartRecording` setting for automatic recording + +**Use Cases:** Meeting archives, compliance documentation, training content creation, interview recordings + +--- + +### Virtual Background + +Replace or blur your real background with AI-powered segmentation for privacy and professionalism. + +**How It Works:** +Virtual backgrounds use machine learning models to segment the person from the background in real-time. The segmentation mask is applied to each video frame, replacing the background with blur or a custom image. + +**Background Options:** +- **Blur**: Gaussian blur applied to background (adjustable intensity via `setVirtualBackgroundBlurLevel`) +- **Image**: Custom image as background (via `setVirtualBackgroundImage`) +- **Clear**: Remove virtual background (via `clearVirtualBackground`) + +**Features:** +- Built-in virtual background settings dialog +- Configurable button visibility +- Real-time background replacement + +**Platform Availability:** JavaScript (Web) only + +**Browser Requirements:** Modern browser with adequate processing power for best results + +**Use Cases:** Work from home privacy, professional video calls, hiding messy rooms, branded backgrounds + +--- + +### Audio Modes + +Switch between different audio output routes on mobile devices. + +**Available Modes:** + +| Mode | Description | +|------|-------------| +| `SPEAKER` | Routes audio through the device loudspeaker | +| `EARPIECE` | Routes audio through the phone earpiece (for private calls) | +| `BLUETOOTH` | Routes audio through a connected Bluetooth device | +| `HEADPHONES` | Routes audio through wired headphones | + +**Features:** +- Set initial audio mode when joining +- Change audio mode dynamically during calls +- Audio mode change events via `MediaEventsListener` +- Automatic detection of connected audio devices +- Configurable audio mode button visibility + +**Platform Availability:** iOS, Android, React Native, Flutter (mobile platforms only) + +**Use Cases:** Switching to speaker during long calls, using Bluetooth in car, private calls with earpiece + +--- + +### Raise Hand + +Virtual hand-raising feature for orderly communication in group calls without verbal interruption. + +**How It Works:** +Participants can raise their hand to signal they want to speak. Other participants see a visual indicator, enabling orderly communication. + +**Features:** +- `raiseHand()` - Show hand-raised indicator +- `lowerHand()` - Remove hand-raised indicator +- Events for when participants raise/lower hands +- Configurable raise hand button visibility + +**Use Cases:** Q&A sessions, classroom discussions, moderated panels, orderly group discussions + +--- + +### Idle Timeout + +Automatic session termination when a participant is alone in a call, preventing abandoned sessions. + +**How It Works:** +When all other participants leave, a countdown timer starts. After the timeout period, a prompt is shown. If no response, the session ends automatically. + +**Configuration:** +- `idleTimeoutPeriodBeforePrompt` - Time before showing the timeout prompt (default: 60 seconds) +- `idleTimeoutPeriodAfterPrompt` - Time after prompt before ending session (default: 120 seconds) + +**Features:** +- Configurable timeout periods +- Session timeout event (`onSessionTimedOut`) +- Can be effectively disabled by setting very long timeout periods + +**Use Cases:** Preventing forgotten calls, resource management, automatic cleanup + +--- + +## Advanced Features + +### Custom Control Panel + +Build a fully customized control panel by hiding default controls and implementing your own UI. + +**How It Works:** +Configure session settings to hide the default control panel (`hideControlPanel: true`), then implement your own buttons that call SDK actions. + +**Customization Options:** +- Hide entire control panel +- Hide individual buttons (mute, video, screen share, recording, etc.) +- Intercept button clicks for custom behavior +- Build completely custom UI with SDK actions + +**Available Actions:** +- `muteAudio()` / `unMuteAudio()` +- `pauseVideo()` / `resumeVideo()` +- `switchCamera()` +- `startScreenSharing()` / `stopScreenSharing()` +- `startRecording()` / `stopRecording()` +- `leaveSession()` + +**Use Cases:** Branded call experience, simplified UI for specific use cases, adding app-specific actions + +--- + +### Picture-in-Picture + +Continue video calls in a floating window while using other content. + +**How It Works:** +Picture-in-Picture uses the browser's Picture-in-Picture API (web) or native PiP frameworks (Android) to display the call video in a floating overlay window. + +**Features:** +- `enablePictureInPictureLayout()` - Enable PiP mode +- `disablePictureInPictureLayout()` - Return to normal view +- Floating video window that stays on top +- Works when switching tabs or apps + +**Browser Support (Web):** +- Chrome 70+ +- Safari 13.1+ +- Edge (Chromium-based) +- Opera 57+ + +**Platform Availability:** JavaScript (Web) and Android only + +**Use Cases:** Multitasking during calls, reference documents while on call, monitoring calls while working + +--- + +### Device Management + +Select and switch between available cameras and microphones during calls. + +**How It Works:** +The SDK enumerates available media devices and allows users to select their preferred input/output devices. Changes take effect immediately without interrupting the call. + +**Features:** +- `getAudioInputDevices()` - List available microphones +- `getAudioOutputDevices()` - List available speakers +- `getVideoInputDevices()` - List available cameras +- `setAudioInputDevice(deviceId)` - Switch microphone +- `setAudioOutputDevice(deviceId)` - Switch speaker +- `setVideoInputDevice(deviceId)` - Switch camera +- Device change events when devices are connected/disconnected +- Built-in settings dialog (`showSettingsDialog()`) + +**Platform Availability:** JavaScript (Web) only + +**Use Cases:** Switching to external webcam, using USB microphone, selecting Bluetooth headset + +--- + +### VoIP Calling + +Native VoIP integration with system-level call handling for a phone-like calling experience. + +**How It Works:** +VoIP calling integrates with the operating system's native call infrastructure (CallKit on iOS, ConnectionService on Android) to provide system-level call UI and lock screen controls. + +**iOS (CallKit) Features:** +- Native iOS incoming call UI (full screen) +- Lock screen call controls +- Call appears in Phone app's Recents +- CarPlay integration +- Do Not Disturb respect + +**Android Features:** +- System notification with call controls +- Foreground service for reliable call handling +- Lock screen call controls + +**Requirements:** +- VoIP push certificate (iOS) +- Push notifications configured +- Platform-specific implementation (CallManager, PushKit delegate) + +**Platform Availability:** iOS, Android, React Native + +**Use Cases:** Phone-like calling experience, reliable incoming calls, car integration + +--- + +### Background Handling + +Keep calls alive when users navigate away from your app on mobile devices. + +**How It Works:** +Background handling uses platform-specific APIs to keep the call active when the user switches to another app or locks their device. On Android, this is achieved through `CometChatOngoingCallService` - a foreground service. + +**Android Features:** +- `CometChatOngoingCallService.launch()` - Start foreground service +- `CometChatOngoingCallService.abort()` - Stop service +- Ongoing notification in status bar +- Tap notification to return to call +- Customizable notification appearance + +**iOS Features:** +- Audio continues in background (requires Background Modes capability) +- CallKit integration for lock screen controls +- Background task handling + +**Required Permissions (Android):** +```xml + + +``` + +**Platform Availability:** iOS, Android, React Native + +**Use Cases:** Multitasking during calls, checking messages mid-call, hands-free calling + +--- + +### In-Call Chat + +Enable text messaging during calls by integrating the chat button with your messaging solution. + +**How It Works:** +The SDK provides a chat button in the control panel and events to help you build a custom chat experience. The actual messaging functionality should be implemented using the CometChat Chat SDK or your own messaging solution. + +**Features:** +- Chat button visibility control (`hideChatButton`) +- Chat button click events (`onChatButtonClicked`) +- Unread message badge (`setChatButtonUnreadCount`) + +**Integration:** +The Calls SDK provides UI hooks, but messaging is handled separately: +1. Show/hide chat button in call UI +2. Listen for chat button clicks +3. Open your chat interface (using CometChat Chat SDK or custom) +4. Update unread badge count + +**Platform Availability:** All platforms + +**Use Cases:** Sharing meeting links, posting questions, sharing code snippets, silent communication + +--- + + +**Platform Availability:** Not all features are available on every platform. See the [Compatibility](/calls/platform/compatibility) page for a detailed feature comparison matrix. + + +## Next Steps + + + + Check platform and browser support + + + Set up user authentication + + diff --git a/calls/platform/overview.mdx b/calls/platform/overview.mdx new file mode 100644 index 00000000..83781f23 --- /dev/null +++ b/calls/platform/overview.mdx @@ -0,0 +1,72 @@ +--- +title: "What is CometChat Calls?" +sidebarTitle: "Overview" +description: "Complete voice and video calling solution built on WebRTC with drop-in UI components." +--- + +import { CardGroup, Card, Callout } from 'mintlify'; + +CometChat Calls SDK provides a complete voice and video calling solution built on WebRTC. It delivers enterprise-grade real-time communication with pre-built UI components, allowing you to add calling functionality to your app in minutes rather than months. + +## Why CometChat Calls? + +Building real-time voice and video calling from scratch requires expertise in WebRTC, media servers, network optimization, and cross-platform development. CometChat handles all this complexity so you can focus on your core product. + +| Benefit | Description | +|---------|-------------| +| **Built-in UI** | Pre-built call screens, controls, and participant views—no UI work required | +| **WebRTC Powered** | Enterprise-grade media infrastructure handles all the complexity | +| **Cross-Platform** | Web, iOS, Android, React Native, and Flutter with consistent APIs | +| **Scalable Infrastructure** | Global media servers with automatic scaling and low latency | + +## How It Works + +1. **Initialize** — Set up the SDK with your App ID and Region +2. **Authenticate** — Log in users with their CometChat auth token +3. **Join Call** — Generate a session token and join the call +4. **Ready** — The SDK renders a complete call UI automatically + + +**Already using CometChat UI Kits?** Voice and video calling is already integrated with ready-to-use components. Use the Calls SDK directly only if you need custom call UI or advanced control. + + +## Use Cases + +CometChat Calls powers real-time communication across industries: + +| Industry | Use Cases | +|----------|-----------| +| **Healthcare** | Telehealth consultations, patient check-ins, specialist referrals | +| **Education** | Virtual classrooms, tutoring sessions, parent-teacher meetings | +| **Marketplace** | Buyer-seller negotiations, customer support, expert consultations | +| **Social** | Video dating, group hangouts, live streaming | +| **Enterprise** | Team meetings, remote interviews, client calls | +| **Gaming** | Voice chat, team coordination, live commentary | + +## What's Included + + + + 1-to-1 and group calls with HD audio/video, adaptive bitrate, and noise cancellation + + + Mute, screen share, recording, layouts, participant management, and more + + + Complete call screens with controls, participant grid, and responsive layouts + + + Native SDKs for JavaScript, iOS, Android, React Native, and Flutter + + + +## Next Steps + + + + Explore all available features in detail + + + Check platform and browser support + + diff --git a/calls/platform/user-sync.mdx b/calls/platform/user-sync.mdx new file mode 100644 index 00000000..13e0fd63 --- /dev/null +++ b/calls/platform/user-sync.mdx @@ -0,0 +1,262 @@ +--- +title: "User Sync" +sidebarTitle: "User Sync" +description: "How to sync users with CometChat for voice and video calling." +--- + +import { CardGroup, Card, Steps, Callout } from 'mintlify'; + +Users must exist in CometChat before they can make or receive calls. This page explains how to create and authenticate users for the Calls SDK. + +## Why User Sync? + +CometChat needs to know about your users to: + +- **Route calls** — Connect callers to the right recipients +- **Identify participants** — Display names and avatars in calls +- **Track history** — Associate call logs with specific users +- **Manage permissions** — Control who can call whom + +## Creating Users + +Choose the method that best fits your workflow: + + + + Add users manually via the CometChat Dashboard. Ideal for quick testing or small teams. + + + Create users programmatically via SDK methods. Perfect for auto-provisioning during sign-up. + + + Create users using the REST API. Best for batch imports or backend workflows. + + + +## User Requirements + +### Required Fields + +| Field | Type | Description | +|:------|:-----|:------------| +| `uid` | String | Unique user identifier. Alphanumeric, max 100 characters. Must be unique across your app. | + +### Optional Fields + +| Field | Type | Description | +|:------|:-----|:------------| +| `name` | String | Display name shown in calls and participant lists | +| `avatar` | String | URL to profile image | +| `metadata` | Object | Custom JSON data for your application | +| `role` | String | User role (default, admin, etc.) | +| `tags` | Array | Tags for categorization and filtering | + +### Example User Object + +```json +{ + "uid": "user_123", + "name": "John Doe", + "avatar": "https://example.com/avatars/john.png", + "metadata": { + "department": "Engineering", + "location": "San Francisco" + } +} +``` + +## Authentication Flow + + + + Create the user in CometChat when they sign up for your app. This only needs to happen once per user. + + **Server-side (recommended):** + ```bash + curl -X POST "https://api-{region}.cometchat.io/v3/users" \ + -H "apiKey: YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"uid": "user_123", "name": "John Doe"}' + ``` + + + + Generate an authentication token for the user on your server. This token is used to log in the user on the client. + + **Server-side:** + ```bash + curl -X POST "https://api-{region}.cometchat.io/v3/users/{uid}/auth_tokens" \ + -H "apiKey: YOUR_API_KEY" \ + -H "Content-Type: application/json" + ``` + + **Response:** + ```json + { + "data": { + "uid": "user_123", + "authToken": "user_123_abc123xyz..." + } + } + ``` + + + + Use the auth token to log in the user via the SDK. This establishes a session for making calls. + + **JavaScript:** + ```javascript + CometChatCalls.login(authToken).then( + user => console.log("Login successful:", user), + error => console.log("Login failed:", error) + ); + ``` + + + + Once logged in, the user can initiate and receive calls. The SDK handles all session management automatically. + + + +## Authentication Methods + +### Auth Token (Recommended for Production) + +Generate tokens server-side using the REST API. This is the secure method for production apps. + +**Pros:** +- Secure — API key never exposed to clients +- Controlled — You decide when tokens are issued +- Auditable — Token generation can be logged + +**Flow:** +1. User logs into your app +2. Your server generates a CometChat auth token +3. Token is sent to the client +4. Client uses token to log into CometChat + +### Auth Key (Development Only) + +Use the Auth Key directly in client code for quick testing. **Never use in production.** + +**JavaScript:** +```javascript +CometChatCalls.login(uid, authKey).then( + user => console.log("Login successful:", user), + error => console.log("Login failed:", error) +); +``` + + +**Security Warning**: Never expose your Auth Key in production client code. Auth Keys have full access to create users and perform admin operations. Always use auth tokens generated server-side for production apps. + + +## Sync Strategies + +### Just-in-Time Provisioning + +Create CometChat users when they first need calling functionality: + +```javascript +// When user initiates their first call +async function ensureUserExists(user) { + try { + // Try to get auth token (user exists) + return await getAuthToken(user.id); + } catch (error) { + // User doesn't exist, create them first + await createCometChatUser(user); + return await getAuthToken(user.id); + } +} +``` + +**Best for:** Apps where not all users need calling + +### Batch Import + +Import existing users via REST API when integrating CometChat: + +```bash +# Create multiple users +curl -X POST "https://api-{region}.cometchat.io/v3/users" \ + -H "apiKey: YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '[ + {"uid": "user_1", "name": "Alice"}, + {"uid": "user_2", "name": "Bob"}, + {"uid": "user_3", "name": "Charlie"} + ]' +``` + +**Best for:** Migrating existing user bases + +### Webhook Sync + +Use webhooks to keep CometChat users in sync with your database: + +1. User signs up → Create CometChat user +2. User updates profile → Update CometChat user +3. User deletes account → Delete CometChat user + +**Best for:** Apps with frequent user changes + +## User Lifecycle + +### Creating Users + +Users can be created via: +- REST API (server-side) +- SDK `createUser` method +- Dashboard (manual) + +### Updating Users + +Update user details when they change in your app: + +```bash +curl -X PUT "https://api-{region}.cometchat.io/v3/users/{uid}" \ + -H "apiKey: YOUR_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"name": "John Smith", "avatar": "https://new-avatar.png"}' +``` + +### Deleting Users + +Remove users when they leave your platform: + +```bash +curl -X DELETE "https://api-{region}.cometchat.io/v3/users/{uid}" \ + -H "apiKey: YOUR_API_KEY" +``` + + +**Soft Delete**: By default, deleted users are soft-deleted and can be restored. Use `permanent=true` query parameter for permanent deletion. + + +## Best Practices + +### Do + +- Generate auth tokens server-side +- Use consistent UIDs across your app +- Keep user data in sync +- Handle token expiration gracefully + +### Don't + +- Expose API keys in client code +- Use Auth Key in production +- Create duplicate users +- Store auth tokens long-term + +## Next Steps + + + + Check platform and browser support + + + Full API documentation for user management + + diff --git a/calls/react-native/overview.mdx b/calls/react-native/overview.mdx index bc6906a3..eb018fe0 100644 --- a/calls/react-native/overview.mdx +++ b/calls/react-native/overview.mdx @@ -8,12 +8,12 @@ The CometChat Calls SDK enables real-time voice and video calling capabilities i **Faster Integration with UI Kits** -If you're using CometChat UI Kits, voice and video calling is already integrated with ready-to-use components: +If you're using CometChat UI Kits, voice and video calling can be quickly integrated: - Incoming & outgoing call screens - Call buttons with one-tap calling - Call logs with history -👉 [React Native UI Kit Call Features](/ui-kit/react-native/call-features) +👉 [React Native UI Kit Calling Integration](/ui-kit/react-native/calling-integration) Use this Calls SDK directly only if you need custom call UI or advanced control. diff --git a/docs.json b/docs.json index a5fe4b8c..36dccdbe 100644 --- a/docs.json +++ b/docs.json @@ -42,10 +42,10 @@ ] }, { - "product": "Chat & Calling", + "product": "Chat & Messaging", "tabs": [ { - "tab": "Chat & Calling", + "tab": "Chat", "pages": [ "chat-call" ] @@ -416,7 +416,8 @@ "ui-kit/react/astro-one-to-one-chat", "ui-kit/react/astro-tab-based-chat" ] - } + }, + "ui-kit/react/calling-integration" ] }, { @@ -843,7 +844,13 @@ "group": " ", "pages": [ "ui-kit/react-native/overview", - "ui-kit/react-native/getting-started", + { + "group": "Getting Started", + "pages": [ + "ui-kit/react-native/getting-started", + "ui-kit/react-native/calling-integration" + ] + }, { "group": "Features", "pages": [ @@ -1149,7 +1156,8 @@ "ui-kit/ios/getting-started", "ui-kit/ios/ios-conversation", "ui-kit/ios/ios-one-to-one-chat", - "ui-kit/ios/ios-tab-based-chat" + "ui-kit/ios/ios-tab-based-chat", + "ui-kit/ios/calling-integration" ] }, { @@ -1468,7 +1476,8 @@ "ui-kit/android/getting-started", "ui-kit/android/android-conversation", "ui-kit/android/android-one-to-one-chat", - "ui-kit/android/android-tab-based-chat" + "ui-kit/android/android-tab-based-chat", + "ui-kit/android/calling-integration" ] }, { @@ -1787,7 +1796,8 @@ "ui-kit/flutter/getting-started", "ui-kit/flutter/flutter-conversation", "ui-kit/flutter/flutter-one-to-one-chat", - "ui-kit/flutter/flutter-tab-based-chat" + "ui-kit/flutter/flutter-tab-based-chat", + "ui-kit/flutter/calling-integration" ] }, { @@ -4814,7 +4824,7 @@ ] }, { - "product": "Calling", + "product": "Voice & Video Calling", "tabs": [ { "tab": "Calling", @@ -4822,6 +4832,15 @@ "calls" ] }, + { + "tab": "Platform", + "pages": [ + "calls/platform/overview", + "calls/platform/features", + "calls/platform/compatibility", + "calls/platform/user-sync" + ] + }, { "tab": "SDK", "tab-id": "calls-sdk", diff --git a/ui-kit/android/call-features.mdx b/ui-kit/android/call-features.mdx index a5deb296..988f6c7e 100644 --- a/ui-kit/android/call-features.mdx +++ b/ui-kit/android/call-features.mdx @@ -6,99 +6,9 @@ title: "Call" CometChat's Calls feature is an advanced functionality that allows you to seamlessly integrate one-on-one as well as group audio and video calling capabilities into your application. This document provides a technical overview of these features, as implemented in the Android UI Kit. -## Integration - -First, make sure that you've correctly integrated the UI Kit library into your project. If you haven't done this yet or are facing difficulties, refer to our [Getting Started](/ui-kit/android/getting-started) guide. This guide will walk you through a step-by-step process of integrating our UI Kit into your Android project. - -Once you've successfully integrated the UI Kit, the next step is to add the CometChat Calls SDK to your project. This is necessary to enable the calling features in the UI Kit. Here's how you do it: - -Add the following dependency to your build.gradle file: - -```javascript -dependencies { - implementation 'com.cometchat:calls-sdk-android:4.+.+' -} -``` - -After adding this dependency, the Android UI Kit will automatically detect it and activate the calling features. Now, your application supports both audio and video calling. You will see [CallButtons](/ui-kit/android/call-buttons) component rendered in [MessageHeader](/ui-kit/android/message-header) Component. - - - - - -To start receive calls globally in your app you will need to add `CallListener`, This needs to be added before the you initialize CometChat UI kit, We sudgest you making a custom Application class and adding call listener. - - - -```java -public class BaseApplication extends Application { - - private static String LISTENER_ID = BaseApplication.class.getSimpleName()+System.currentTimeMillis(); - - @Override - public void onCreate() { - super.onCreate(); - CometChat.addCallListener(LISTENER_ID, new CometChat.CallListener() { - @Override - public void onIncomingCallReceived(Call call) { - CometChatCallActivity.launchIncomingCallScreen(BaseApplication.this, call, null); //pass null or IncomingCallConfiguration if need to configure CometChatIncomingCall component - } - - @Override - public void onOutgoingCallAccepted(Call call) { - - } - - @Override - public void onOutgoingCallRejected(Call call) { - - } - - @Override - public void onIncomingCallCancelled(Call call) { - - } - }); - } -} -``` - - - - -```kotlin -class BaseApplication : Application() { - companion object { - private val LISTENER_ID = "${BaseApplication::class.java.simpleName}${System.currentTimeMillis()}" - } - - override fun onCreate() { - super.onCreate() - CometChat.addCallListener(LISTENER_ID, object : CometChat.CallListener { - override fun onIncomingCallReceived(call: Call) { - CometChatCallActivity.launchIncomingCallScreen(this@BaseApplication, call, null) - // Pass null or IncomingCallConfiguration if need to configure CometChatIncomingCall component - } - - override fun onOutgoingCallAccepted(call: Call) { - // To be implemented - } - - override fun onOutgoingCallRejected(call: Call) { - // To be implemented - } - - override fun onIncomingCallCancelled(call: Call) { - // To be implemented - } - }) - } -} -``` - - - - + +If you haven't set up calling yet, follow the [Calling Integration](/ui-kit/android/calling-integration) guide first. + ## Features diff --git a/ui-kit/android/calling-integration.mdx b/ui-kit/android/calling-integration.mdx new file mode 100644 index 00000000..078b0eac --- /dev/null +++ b/ui-kit/android/calling-integration.mdx @@ -0,0 +1,102 @@ +--- +title: "Calling Integration" +description: "Add voice and video calling to your Android UI Kit application" +--- + +## Overview + +This guide walks you through adding voice and video calling capabilities to your Android application using the CometChat UI Kit. + + +Make sure you've completed the [Getting Started](/ui-kit/android/getting-started) guide before proceeding. + + +## Add the Calls SDK + +Add the CometChat Calls SDK dependency to your `build.gradle` file: + +```groovy +dependencies { + implementation 'com.cometchat:calls-sdk-android:4.+.+' +} +``` + +After adding this dependency, the Android UI Kit will automatically detect it and activate the calling features. You will see [CallButtons](/ui-kit/android/call-buttons) component rendered in the [MessageHeader](/ui-kit/android/message-header) component. + + + + + +## Set Up Call Listener + +To receive incoming calls globally in your app, add a `CallListener` before initializing the CometChat UI Kit. We recommend creating a custom Application class: + + + +```kotlin +class BaseApplication : Application() { + companion object { + private val LISTENER_ID = "${BaseApplication::class.java.simpleName}${System.currentTimeMillis()}" + } + + override fun onCreate() { + super.onCreate() + CometChat.addCallListener(LISTENER_ID, object : CometChat.CallListener { + override fun onIncomingCallReceived(call: Call) { + CometChatCallActivity.launchIncomingCallScreen(this@BaseApplication, call, null) + // Pass null or IncomingCallConfiguration if need to configure CometChatIncomingCall component + } + + override fun onOutgoingCallAccepted(call: Call) { + // Handle accepted outgoing call + } + + override fun onOutgoingCallRejected(call: Call) { + // Handle rejected outgoing call + } + + override fun onIncomingCallCancelled(call: Call) { + // Handle cancelled incoming call + } + }) + } +} +``` + + + +```java +public class BaseApplication extends Application { + + private static String LISTENER_ID = BaseApplication.class.getSimpleName() + System.currentTimeMillis(); + + @Override + public void onCreate() { + super.onCreate(); + CometChat.addCallListener(LISTENER_ID, new CometChat.CallListener() { + @Override + public void onIncomingCallReceived(Call call) { + CometChatCallActivity.launchIncomingCallScreen(BaseApplication.this, call, null); + // Pass null or IncomingCallConfiguration if need to configure CometChatIncomingCall component + } + + @Override + public void onOutgoingCallAccepted(Call call) { + // Handle accepted outgoing call + } + + @Override + public void onOutgoingCallRejected(Call call) { + // Handle rejected outgoing call + } + + @Override + public void onIncomingCallCancelled(Call call) { + // Handle cancelled incoming call + } + }); + } +} +``` + + diff --git a/ui-kit/flutter/call-features.mdx b/ui-kit/flutter/call-features.mdx index 12559e90..1344b875 100644 --- a/ui-kit/flutter/call-features.mdx +++ b/ui-kit/flutter/call-features.mdx @@ -6,199 +6,9 @@ title: "Call" CometChat's Calls feature is an advanced functionality that allows you to seamlessly integrate one-on-one as well as group audio and video calling capabilities into your application. This document provides a technical overview of these features, as implemented in the Flutter UI Kit. -## Integration - -First, make sure that you've correctly integrated the UI Kit library into your project. If you haven't done this yet or are facing difficulties, refer to our [Getting Started](/ui-kit/flutter/getting-started) guide. This guide will walk you through a step-by-step process of integrating our UI Kit into your Flutter project. - -Once you've successfully integrated the UI Kit, the next step is to add the CometChat Calls SDK to your project. This is necessary to enable the calling features in the UI Kit. Here's how you do it: - -Step 1 - -### Add Dependency - -Add the following dependency to your `pubspec.yaml` file: - - - -```dart -dependencies: - cometchat_calls_uikit: ^5.0.12 -``` - - - - - -*** - -Step 2 - -### Update build.gradle - -If your Flutter project's minimum Android SDK version (minSdkVersion) is below API level 24, you should update it to at least 24. To achieve this, navigate to the `android/app/build.gradle` file and modify the `minSdkVersion` property within the `defaultConfig` section. - - - -```gradle -defaultConfig { - minSdkVersion 24 - targetSdkVersion flutter.targetSdkVersion - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName -} -``` - - - - - - - -If you want to use the Flutter UI Kit or enable calling support within it, you'll need to: - -1. Set the `minSdkVersion` to 24 in your `android/app/build.gradle` file. - - - -*** - -Step 3 - -### Update iOS Podfile - -In your Podfile located at `ios/Podfile`, update the minimum iOS version that your project supports to 12. - - - -```xml -platform :ios, '12.0' -``` - - - - - -*** - -Step 4 - -### Modify UIKitSettings - -To activate the calling features, you'll need to modify the UIKitSettings using `callingExtension` and pass the key in the widget. - -Example - - - -```dart -UIKitSettings uiKitSettings = (UIKitSettingsBuilder() - ..subscriptionType = CometChatSubscriptionType.allUsers - ..autoEstablishSocketConnection = true - ..region = "REGION"//Replace with your App Region - ..appId = "APP_ID" //Replace with your App ID - ..authKey = "AUTH_KEY" //Replace with your app Auth Key - ..extensions = CometChatUIKitChatExtensions.getDefaultExtensions() //Replace this with empty array you want to disable all extensions - ..callingExtension = CometChatCallingExtension() //Added this to Enable calling feature in the UI Kit -).build(); - -CometChatUIKit.init(uiKitSettings: uiKitSettings,onSuccess: (successMessage) { - // TODO("Not yet implemented") -}, onError: (e) { - // TODO("Not yet implemented") -}); -``` - - - - - -To allow launching of Incoming Call screen from any widget whenever a call is received provide set key: CallNavigationContext.navigatorKey in the top most widget of your project (the widget that appears first of your app launch). - - - -```dart -CometChatUIKit.login(uid, onSuccess: (s) { - Navigator.push(context, MaterialPageRoute(builder: (context) => CometChatUsersWithMessages(key: CallNavigationContext.navigatorKey))); -}, onError: (e) { - // TODO("Not yet implemented") -}); -``` - - - - - -After adding this dependency, the Flutter UI Kit will automatically detect it and activate the calling features. Now, your application supports both audio and video calling. You will see [CallButtons](/ui-kit/flutter/call-buttons) widget rendered in [MessageHeader](/ui-kit/flutter/message-header) Widget. - - - - - -### Listeners - -For every top-level widget you wish to receive the call events in, you need to register the CallListener listener using the `addCallListener()` method. - - - -```dart -import 'package:cometchat/cometchat_sdk.dart'; - -class _YourClassNameState extends State with CallListener { - @override - void initState() { - super.initState(); - CometChat.addCallListener(listenerId, this); //Add listener - } - - @override - void dispose() { - super.dispose(); - CometChat.removeCallListener(listenerId); //Remove listener - } - - @override - void onIncomingCallCancelled(Call call) { - super.onIncomingCallCancelled(call); - debugPrint("onIncomingCallCancelled"); - } - - @override - void onIncomingCallReceived(Call call) { - super.onIncomingCallReceived(call); - debugPrint("onIncomingCallReceived"); - } - - @override - void onOutgoingCallAccepted(Call call) { - super.onOutgoingCallAccepted(call); - debugPrint("onOutgoingCallAccepted"); - } - - @override - void onOutgoingCallRejected(Call call) { - super.onOutgoingCallRejected(call); - debugPrint("onOutgoingCallRejected"); - } - - @override - void onCallEndedMessageReceived(Call call) { - super.onCallEndedMessageReceived(call); - debugPrint("onCallEndedMessageReceived"); - } - - @override - Widget build(BuildContext context) { - // TODO: implement build - throw UnimplementedError(); - } -} -``` - - - - - -*** + +If you haven't set up calling yet, follow the [Calling Integration](/ui-kit/flutter/calling-integration) guide first. + ## Features diff --git a/ui-kit/flutter/calling-integration.mdx b/ui-kit/flutter/calling-integration.mdx new file mode 100644 index 00000000..5c39b698 --- /dev/null +++ b/ui-kit/flutter/calling-integration.mdx @@ -0,0 +1,145 @@ +--- +title: "Calling Integration" +description: "Add voice and video calling to your Flutter UI Kit application" +--- + +## Overview + +This guide walks you through adding voice and video calling capabilities to your Flutter application using the CometChat UI Kit. + + +Make sure you've completed the [Getting Started](/ui-kit/flutter/getting-started) guide before proceeding. + + +## Step 1: Add Dependency + +Add the following dependency to your `pubspec.yaml` file: + +```yaml +dependencies: + cometchat_calls_uikit: ^5.0.12 +``` + +## Step 2: Update Android build.gradle + +If your Flutter project's minimum Android SDK version is below API level 24, update it in `android/app/build.gradle`: + +```groovy +defaultConfig { + minSdkVersion 24 + targetSdkVersion flutter.targetSdkVersion + versionCode flutterVersionCode.toInteger() + versionName flutterVersionName +} +``` + +## Step 3: Update iOS Podfile + +In `ios/Podfile`, update the minimum iOS version to 12: + +```ruby +platform :ios, '12.0' +``` + +## Step 4: Enable Calling in UIKitSettings + +Modify the UIKitSettings to activate calling features using `callingExtension`: + +```dart +UIKitSettings uiKitSettings = (UIKitSettingsBuilder() + ..subscriptionType = CometChatSubscriptionType.allUsers + ..autoEstablishSocketConnection = true + ..region = "REGION" // Replace with your App Region + ..appId = "APP_ID" // Replace with your App ID + ..authKey = "AUTH_KEY" // Replace with your Auth Key + ..extensions = CometChatUIKitChatExtensions.getDefaultExtensions() + ..callingExtension = CometChatCallingExtension() // Enable calling +).build(); + +CometChatUIKit.init(uiKitSettings: uiKitSettings, onSuccess: (successMessage) { + // Success +}, onError: (e) { + // Error +}); +``` + +## Step 5: Set Up Incoming Call Navigation + +To allow launching the Incoming Call screen from any widget, provide the `CallNavigationContext.navigatorKey` in your top-level widget: + +```dart +CometChatUIKit.login(uid, onSuccess: (s) { + Navigator.push(context, MaterialPageRoute( + builder: (context) => CometChatUsersWithMessages( + key: CallNavigationContext.navigatorKey + ) + )); +}, onError: (e) { + // Error +}); +``` + +## Verify Integration + +After adding the dependency, the Flutter UI Kit will automatically detect it and activate calling features. You will see [CallButtons](/ui-kit/flutter/call-buttons) rendered in the [MessageHeader](/ui-kit/flutter/message-header) widget. + + + + + +## Set Up Call Listeners + +For every top-level widget where you want to receive call events, register the CallListener: + +```dart +import 'package:cometchat/cometchat_sdk.dart'; + +class _YourClassNameState extends State with CallListener { + @override + void initState() { + super.initState(); + CometChat.addCallListener(listenerId, this); + } + + @override + void dispose() { + super.dispose(); + CometChat.removeCallListener(listenerId); + } + + @override + void onIncomingCallReceived(Call call) { + super.onIncomingCallReceived(call); + debugPrint("onIncomingCallReceived"); + } + + @override + void onOutgoingCallAccepted(Call call) { + super.onOutgoingCallAccepted(call); + debugPrint("onOutgoingCallAccepted"); + } + + @override + void onOutgoingCallRejected(Call call) { + super.onOutgoingCallRejected(call); + debugPrint("onOutgoingCallRejected"); + } + + @override + void onIncomingCallCancelled(Call call) { + super.onIncomingCallCancelled(call); + debugPrint("onIncomingCallCancelled"); + } + + @override + void onCallEndedMessageReceived(Call call) { + super.onCallEndedMessageReceived(call); + debugPrint("onCallEndedMessageReceived"); + } + + @override + Widget build(BuildContext context) { + // Your widget build + } +} +``` diff --git a/ui-kit/ios/call-features.mdx b/ui-kit/ios/call-features.mdx index 7506c32f..9930149b 100644 --- a/ui-kit/ios/call-features.mdx +++ b/ui-kit/ios/call-features.mdx @@ -6,96 +6,10 @@ title: "Call" CometChat's Calls feature is an advanced functionality that allows you to seamlessly integrate one-on-one as well as group audio and video calling capabilities into your application. This document provides a technical overview of these features, as implemented in the iOS UI Kit. -## Integration - -First, make sure that you've correctly integrated the UI Kit library into your project. If you haven't done this yet or are facing difficulties, refer to our [Getting Started](/ui-kit/ios/getting-started) guide. This guide will walk you through a step-by-step process of integrating our UI Kit into your iOS project. - -Once you've successfully integrated the UI Kit, the next step is to add the CometChat Calls SDK to your project. This is necessary to enable the calling features in the UI Kit. Here's how you do it: - -**1. CocoaPods** - -We recommend using CocoaPods, as they are the most advanced way of managing iOS project dependencies. Open a terminal window, move to your project directory, and then create a Podfile by running the following command. - - - -1. You can install CometChatCallsSDK for iOS through Swift Package Manager or Cocoapods - -2. CometChatCallsSDK supports iOS 13 and aboveSwift 5.0+ - -3. CometChatCallsSDK supports Swift 5.0+ - - - -```ruby Swift -$ pod init -``` - -Add the following lines to the Podfile. - -```ruby Swift -platform :ios, '11.0' -use_frameworks! - -target 'YourApp' do - pod 'CometChatUIKitSwift', '4.3.15' - pod 'CometChatCallsSDK', '4.0.6' -end -``` - -And then install the CometChatCallsSDK framework through CocoaPods. - -```ruby Swift -$ pod install -``` - -If you're facing any issues while installing pods then use the below command. - -```ruby Swift -$ pod install --repo-update -``` - -Always get the latest version of CometChatCallsSDK by command. - -```ruby Swift -$ pod update CometChatCallsSDK -``` - - - -Always ensure to open the XCFramework file after adding the dependencies. - - - -*** - -**2. Swift Package Manager.** - -Add the Call SDK dependency : - -You can install **Calling SDK for iOS** through **Swift Package Manager.** - -1. Go to your Swift Package Manager's File tab and select Add Packages - -2. Add `CometChatCallsSDK` into your `Package Repository` as below: - -```sh Bash -https://github.com/cometchat/calls-sdk-ios.git -``` - -3. To add the package, select Version Rules, enter Up to Exact Version, **4.0.5** and click Next. - - -Before Adding the Call SDK dependency to your project's dependencies please make sure that you have completed the Chat UI Kit Integration. - +If you haven't set up calling yet, follow the [Calling Integration](/ui-kit/ios/calling-integration) guide first. -After adding this dependency, the iOS UI Kit will automatically detect it and activate the calling features. Now, your application supports both audio and video calling. You will see [CallButtons](/ui-kit/ios/call-buttons) component rendered in [MessageHeader](/ui-kit/ios/message-header) Component. - - - - - ## Features ### Incoming Call diff --git a/ui-kit/ios/calling-integration.mdx b/ui-kit/ios/calling-integration.mdx new file mode 100644 index 00000000..c373888e --- /dev/null +++ b/ui-kit/ios/calling-integration.mdx @@ -0,0 +1,86 @@ +--- +title: "Calling Integration" +description: "Add voice and video calling to your iOS UI Kit application" +--- + +## Overview + +This guide walks you through adding voice and video calling capabilities to your iOS application using the CometChat UI Kit. + + +Make sure you've completed the [Getting Started](/ui-kit/ios/getting-started) guide before proceeding. + + +## Add the Calls SDK + +You can install the CometChat Calls SDK using either CocoaPods or Swift Package Manager. + + +- CometChatCallsSDK supports iOS 13 and above +- CometChatCallsSDK supports Swift 5.0+ + + +### Option 1: CocoaPods + +Open a terminal window, navigate to your project directory, and create a Podfile: + +```ruby +$ pod init +``` + +Add the following lines to the Podfile: + +```ruby +platform :ios, '13.0' +use_frameworks! + +target 'YourApp' do + pod 'CometChatUIKitSwift', '4.3.15' + pod 'CometChatCallsSDK', '4.0.6' +end +``` + +Install the dependencies: + +```ruby +$ pod install +``` + +If you encounter issues, try: + +```ruby +$ pod install --repo-update +``` + +To update to the latest version: + +```ruby +$ pod update CometChatCallsSDK +``` + + +Always open the `.xcworkspace` file after adding the dependencies. + + +### Option 2: Swift Package Manager + +1. Go to File → Add Packages in Xcode +2. Add the CometChat Calls SDK repository: + +``` +https://github.com/cometchat/calls-sdk-ios.git +``` + +3. Select Version Rules, enter the version (e.g., 4.0.5), and click Next + + +Make sure you have completed the Chat UI Kit integration before adding the Calls SDK. + + +## Verify Integration + +After adding the dependency, the iOS UI Kit will automatically detect it and activate calling features. You will see [CallButtons](/ui-kit/ios/call-buttons) rendered in the [MessageHeader](/ui-kit/ios/message-header) component. + + + + diff --git a/ui-kit/react-native/call-features.mdx b/ui-kit/react-native/call-features.mdx index 7d3fef3d..ea0dafe7 100644 --- a/ui-kit/react-native/call-features.mdx +++ b/ui-kit/react-native/call-features.mdx @@ -2,221 +2,13 @@ title: "Call" --- -CometChat’s Calls feature offers advanced functionality for seamlessly integrating one-on-one and group audio/video calling into your application. This document provides a technical overview of how these features are implemented using the React Native UI Kit. +## Overview -## Integration +CometChat's Calls feature offers advanced functionality for seamlessly integrating one-on-one and group audio/video calling into your application. This document provides a technical overview of how these features are implemented using the React Native UI Kit. -First, make sure that you've correctly integrated the UI Kit library into your project. If you haven't done this yet or are facing difficulties, refer to our [Getting Started](/ui-kit/react-native/getting-started) guide. This guide will walk you through a step-by-step process of integrating our UI Kit into your React Native project. - -Once you've successfully integrated the UI Kit, the next step is to add the CometChat Calls SDK to your project. This is necessary to enable the calling features in the UI Kit. Here's how you do it: - - - -```sh -npm install @cometchat/calls-sdk-react-native -``` - - - - - -Once the dependency is added, the React Native UI Kit will automatically detect it and enable calling features. Your application will now support both audio and video calls and the [CallButtons](/ui-kit/react-native/call-buttons) component will appear within the [MessageHeader](/ui-kit/react-native/message-header) component. - - - -![Image](/images/81959c4b-Calling-ee689247c8cdd512c520b85f30683ad8.png) - - - - -![Image](/images/bae9d77f-call_overview_cometchat_screens-a20afca1663c9bd893005bd9bb0fffeb.png) - - - - - -## Add necessary permissions - -### Android: - -Go to AndroidManifest.xml located in the react-native/app/src/main of your React Native project and add the following permissions: - - - -```xml - - - - - - - - - - - - - - -``` - - - - - -### iOS: - -Open Info.plist located in the ios/\{appname} of your React Native project and add the following entries: - - - -```xml - - - - - NSCameraUsageDescription - Access camera - NSMicrophoneUsageDescription - Access Microphone - - -``` - - - - - -## Setup minimum Android and iOS versions - -For Android go to app-level build.gradle and change minSdkVersion to 24 and the targetSdkVersion and compileSdkVersion to 33. - - - -```gradle -android { - namespace "com.org.name_of_your_app" - - compileSdkVersion 33 - - defaultConfig { - - minSdkVersion 24 - - targetSdkVersion 33 - - } - -} -``` - - - - - -For iOS you can open xcworkspace in XCode modify the IPHONEOS\_DEPLOYMENT\_TARGET to 12.0 - -or you can specify it in the post\_install hook of the Podfile - - - -```ruby -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - target.build_configurations.each do |build_configuration| - - build_configuration.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' - - build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' - build_configuration.build_settings['ENABLE_BITCODE'] = 'NO' - end - end -end -``` - - - - - -## Add the Call Listeners - -In addition to CallButtons, the Calls UI Kit offers fully functional UI components for handling Incoming, Outgoing, and Ongoing calls. To receive call events in your desired component or screen, you must register a call listener using the addCallListener() method. The onIncomingCallReceived() event is triggered whenever an incoming call is received. - - - -```tsx -import React, { useEffect, useRef, useState } from "react"; -import { SafeAreaView } from "react-native"; -import { CometChat } from "@cometchat/chat-sdk-react-native"; -import { CometChatIncomingCall } from "@cometchat/chat-uikit-react-native"; - -// Track whether the user is logged in -const [loggedIn, setLoggedIn] = useState(false); -// Track if there is an incoming call to display -const [callReceived, setCallReceived] = useState(false); -// Store the incoming call object for use in the UI -const incomingCall = useRef( - null -); -// Unique ID for registering and removing the call listener -var listenerID: string = "UNIQUE_LISTENER_ID"; - -const App = (): React.ReactElement => { - useEffect(() => { - // Register the call listener when the component mounts or when login state changes - CometChat.addCallListener( - listenerID, - new CometChat.CallListener({ - // Fired when an incoming call is received - onIncomingCallReceived: (call: CometChat.Call) => { - // Store the incoming call and update state. - incomingCall.current = call; - // Trigger UI to show incoming call screen - setCallReceived(true); - }, - // Fired when an outgoing call is rejected by the recipient - onOutgoingCallRejected: () => { - // Clear the call state if outgoing call is rejected. - incomingCall.current = null; // Clear the call object - setCallReceived(false); // Hide any call UI - }, - onIncomingCallCancelled: () => { - // Clear the call state if the incoming call is cancelled. - incomingCall.current = null; - setCallReceived(false); - }, - }) - ); - - // Cleanup: remove the call listener when the component unmounts or before re-running - return () => { - CometChat.removeCallListener(listenerID); - }; - }, [loggedIn]); // Re-run effect if the login state changes - - return ( - - {/* Render the incoming call UI when logged in and a call has been received */} - {loggedIn && callReceived && incomingCall.current ? ( - { - // Handle call decline by clearing the incoming call state. - incomingCall.current = null; // Clear the call object - setCallReceived(false); // Hide the incoming call UI - }} - /> - ) : null} - - ); -}; -``` - - - - + +If you haven't set up calling yet, follow the [Calling Integration](/ui-kit/react-native/calling-integration) guide first. + ## Features diff --git a/ui-kit/react-native/calling-integration.mdx b/ui-kit/react-native/calling-integration.mdx new file mode 100644 index 00000000..ea9703eb --- /dev/null +++ b/ui-kit/react-native/calling-integration.mdx @@ -0,0 +1,157 @@ +--- +title: "Calling Integration" +description: "Add voice and video calling to your React Native UI Kit application" +--- + +## Overview + +This guide walks you through adding voice and video calling capabilities to your React Native application using the CometChat UI Kit. + + +Make sure you've completed the [Getting Started](/ui-kit/react-native/getting-started) guide before proceeding. + + +## Add the Calls SDK + +Install the CometChat Calls SDK: + +```bash +npm install @cometchat/calls-sdk-react-native +``` + +Once added, the React Native UI Kit will automatically detect it and enable calling features. The [CallButtons](/ui-kit/react-native/call-buttons) component will appear within the [MessageHeader](/ui-kit/react-native/message-header) component. + + + + + + + + + + + + + + +## Add Permissions + +### Android + +Add the following permissions to `android/app/src/main/AndroidManifest.xml`: + +```xml + + + + + + + + + + + + +``` + +### iOS + +Add the following entries to `ios/{appname}/Info.plist`: + +```xml +NSCameraUsageDescription +Access camera +NSMicrophoneUsageDescription +Access Microphone +``` + +## Set Up Minimum Versions + +### Android + +In `android/app/build.gradle`, set the SDK versions: + +```groovy +android { + compileSdkVersion 33 + + defaultConfig { + minSdkVersion 24 + targetSdkVersion 33 + } +} +``` + +### iOS + +In Xcode, set `IPHONEOS_DEPLOYMENT_TARGET` to 12.0, or add to your Podfile: + +```ruby +post_install do |installer| + installer.pods_project.targets.each do |target| + target.build_configurations.each do |build_configuration| + build_configuration.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '12.0' + build_configuration.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'arm64 i386' + build_configuration.build_settings['ENABLE_BITCODE'] = 'NO' + end + end +end +``` + +## Set Up Call Listeners + +Register a call listener to receive call events in your component: + +```tsx +import React, { useEffect, useRef, useState } from "react"; +import { SafeAreaView } from "react-native"; +import { CometChat } from "@cometchat/chat-sdk-react-native"; +import { CometChatIncomingCall } from "@cometchat/chat-uikit-react-native"; + +const App = (): React.ReactElement => { + const [loggedIn, setLoggedIn] = useState(false); + const [callReceived, setCallReceived] = useState(false); + const incomingCall = useRef(null); + const listenerID = "UNIQUE_LISTENER_ID"; + + useEffect(() => { + CometChat.addCallListener( + listenerID, + new CometChat.CallListener({ + onIncomingCallReceived: (call: CometChat.Call) => { + incomingCall.current = call; + setCallReceived(true); + }, + onOutgoingCallRejected: () => { + incomingCall.current = null; + setCallReceived(false); + }, + onIncomingCallCancelled: () => { + incomingCall.current = null; + setCallReceived(false); + }, + }) + ); + + return () => { + CometChat.removeCallListener(listenerID); + }; + }, [loggedIn]); + + return ( + + {loggedIn && callReceived && incomingCall.current ? ( + { + incomingCall.current = null; + setCallReceived(false); + }} + /> + ) : null} + + ); +}; +``` diff --git a/ui-kit/react/call-features.mdx b/ui-kit/react/call-features.mdx index f9d61886..92de7d97 100644 --- a/ui-kit/react/call-features.mdx +++ b/ui-kit/react/call-features.mdx @@ -6,23 +6,9 @@ title: "Call" CometChat's Calls feature is an advanced functionality that allows you to seamlessly integrate one-on-one as well as group audio and video calling capabilities into your application. This document provides a technical overview of these features, as implemented in the React UI Kit. -## Integration - -First, make sure that you've correctly integrated the UI Kit library into your project. If you haven't done this yet or are facing difficulties, refer to our [Getting Started](/ui-kit/react/integration) guide. This guide will walk you through a step-by-step process of integrating our UI Kit into your React project. - -Once you've successfully integrated the UI Kit, the next step is to add the CometChat Calls SDK to your project. This is necessary to enable the calling features in the UI Kit. Here's how you do it: - -Add the SDK files to your project's dependencies or libraries: - -```java -npm install @cometchat/calls-sdk-javascript -``` - -After adding this dependency, the React UI Kit will automatically detect it and activate the calling features. Now, your application supports both audio and video calling. You will see [CallButtons](/ui-kit/react/call-buttons) component rendered in [MessageHeader](/ui-kit/react/message-header) Component. - - - - + +If you haven't set up calling yet, follow the [Calling Integration](/ui-kit/react/calling-integration) guide first. + ## Features diff --git a/ui-kit/react/calling-integration.mdx b/ui-kit/react/calling-integration.mdx new file mode 100644 index 00000000..3d0365d8 --- /dev/null +++ b/ui-kit/react/calling-integration.mdx @@ -0,0 +1,28 @@ +--- +title: "Calling Integration" +description: "Add voice and video calling to your React UI Kit application" +--- + +## Overview + +This guide walks you through adding voice and video calling capabilities to your React application using the CometChat UI Kit. + + +Make sure you've completed the [Getting Started](/ui-kit/react/react-js-integration) guide before proceeding. + + +## Add the Calls SDK + +Install the CometChat Calls SDK: + +```bash +npm install @cometchat/calls-sdk-javascript +``` + +## Verify Integration + +After adding the dependency, the React UI Kit will automatically detect it and activate calling features. You will see [CallButtons](/ui-kit/react/call-buttons) rendered in the [MessageHeader](/ui-kit/react/message-header) component. + + + + From 7550d39de6ba0549b407be83a0b549a89620ae35 Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Fri, 16 Jan 2026 23:14:09 +0530 Subject: [PATCH 09/10] docs(calls): streamline Calls documentation and update hero banner - Remove redundant callout about UI Kits integration - Reduce Resources section CardGroup from 3 to 2 columns - Remove REST API Reference card from Resources section - Update voice-video-banner.png with improved visual design - Simplify documentation structure for better clarity and focus --- calls.mdx | 9 +-------- images/voice-video-banner.png | Bin 469591 -> 285843 bytes 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/calls.mdx b/calls.mdx index 76ed9a75..a15a98dc 100644 --- a/calls.mdx +++ b/calls.mdx @@ -60,10 +60,6 @@ import { CardGroup, Card, Steps, Columns, AccordionGroup, Accordion, Callout } f Enterprise-grade media servers handle all complexity - - -**Already using CometChat UI Kits?** Voice and video calling is already integrated with ready-to-use components. Use the Calls SDK directly only if you need custom call UI or advanced control. -
@@ -157,13 +153,10 @@ import { CardGroup, Card, Steps, Columns, AccordionGroup, Accordion, Callout } f

Resources

- + Features, compatibility, and platform comparison - - Server-side APIs for call management and recordings - Troubleshooting guides and FAQs diff --git a/images/voice-video-banner.png b/images/voice-video-banner.png index 2e316044de18c20d202a8185c91ef128ca593d6e..dcb18ae1d8692d09abf9e58261ae39d78a602fd8 100644 GIT binary patch literal 285843 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Ð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

  • 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�?>wYK@McJWIQGY?H)t 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

    TciFtEt`qx5)`deQ zLb|9kj1NEDM!EJmE)94G?5n8$-ZLkeF%2DV|RO zk%p!4#=+OmVUY?WhX#Hs0|7E~I}1o-6A&5z8rE4hP+LvO^E_g(-=R2nh~MT#HvNa$sH|Lf-}1hxXu7(TwN@e?I96x1Hh8yh1lrft%si@WbKq@#9x#_zXl2GG)vLGyZ?UnjCbx=N=O^+SNN zc3ApF3>c}lIeiq{=G+Bk!Yca=lgHL%V}GVfJbavTKCYZ9i)>NC^wl5WGx+hzr14Ab z=0~>lj5YPp<>Xwc{?C*_9bPFP{}D@H41JoG7p2WtvMF1%B;!)7JhIi$Jw0t>3oa!Y z2JyCM;#>W<6ShM|YxoX&v(~-&QXR!5-M#kn!hw%BF!@(5Qr-94{43oXgc~2J=WzT_ zB@>52#V$V^{~#Ei85wZ?=iV6yJ0vBGN$W?-P@3%wsr_z@EIO?~va`J%k|a^i8-4$S zc`FNsC{`^+k*JGYkfW8t*traLQ|cewaVA_n!ZgerEfQkr2Yx|hkW0;wOx*2iZ2OKB zvmqhj_r=0W90|%Y^Tx$Q+yfq$X-)-Vi>ABn(qO37e3|{W3XOW?2?PbiEgO7OB?|b~ zdol5^lMl0VhIGBSx4Hq@oF44V_kupOXcJI&1JrIoT5tg@2FJyx-Xo3nQ<}}x4OaN+ zsl;GXeyd-NMnR07p3$Fayd<8iW^Dfi>>}$>VaddayP->|bGaE_fSC5VakoqzP~g0Q zWuJEm08fqFmA!K_u6Yh6#;Pw1iZg+Tr?-`nO&m+TxPO__xQxl&>DUqWXgn}OUK|K|j@ z4GSCew&?;WyiV$wS^OhMCL!kax+N6n6b~WWI!8EFc;Sr&b8Say?7Z+TW5;~Ghrn{m zf;YL9Ggc-oafyhyuxoU-x>-uB0zROwc4kU~OlPiL^u}apb`g%j zF#|aTjbS_IPZ#kAFEu0-X`YvcDC*PCCnbB=QQ-0O6tP}Q-|{W)DM;+~dMhN}CL*rr zNpI7M&6$++qSu*cw0H_LRO2n_d^kE9`S?WMA#`_TFWEz&`Fh{Xb>!6|ee|x8QB$2> z%j#>X7SJH$e~&6I+Oq%abGhNm#Wa(5FD<~Owb#%w0JK657rAy`@Dg0iO zA*h#R{lNw#q03)qsZHcS4KB8^W$vAxC228>!KD6mS+-*J|FZxh^g9lJ)uIJW2}hVO z$#;q)cQ+cFMP9&*UYb)cbDT>UW-wcIed{O4^DL*GqhW-n<8!FXchy5qg5>4t)5nSP zaym<=XDoebOf&mH%4KaQ|8<&usKe$8XDk6InJ*b+V+Xe%L^jF#%RK(F*IMI@+5OS0XQLUDn{03 zkT8eu!)l7o5v;t(JU?JS(*7$)j4y%GAITY`$ug#zh~4lEECOQ8aLSc^6r8fv9jCr^ z`(3?b9+dji&%;H}KmuE>9~Fr;bEruG01soy!CwA48>-=^$c1Sbv`;2N5>lxP@MOLj zql}&iMs6ZV#8AXdsf*hEFe*WA8p6AMet?zBl++Mb^l>W9&R-}tC}pdF=Bvmg>E?y= zC^7gm6Std4<@N5Gp?duGuenlF);(hCR8meX`Fg=McYp?B zJACbIG+R5&g|dNyis(}Ph(l_G3oZXgp?)hh#qm2*++g`stz_m)balZfViOl<-8dLV zi2%(p)S!hfJ6}o{6Y3<(-S>2xael6+Es{<#6tZ(dkUZC8J%#L6CDnioHt~LVN0!z> zX@w`2d>he@D{}?CI&XF71!VMJj&^9G&XEKa<(m=wiWphkBXodKO`c>=CrpR;I*JFA zz(D%-nTQ%}T0-1cF7|PwUr4{-^dGM>YfZohn>WbnK)exmmoVNe<0RmCTH?bC&gvg3 z!t<=g(#Qn%c`QLImzjPzt(o70`EIQ#*B&bzU4VDc-y0#{E6y;|rhAgbXP+L{dD@NS zUNpJS0WD}8Gl)%Q{tv^R3Rn-4*eiEx*-cJwaJxn8$-ry2`|hS4;`wo&pKrU-J>$e~ z^%fU!ROtOk&i0WZ$D>0Q8=N+Ng3sY&zP!(wuaC~w_^g$g;48Ei*FRz_zEiUhYY#wY zUc?`L#@@oIgY#k+V|Xma#bpib{g|jYDdyycnUKAACojqC zmMR30J$C_{fBk(24gnw8X)Zkt@S6bj7#Ot&7d;pUXRWu=RBrpML88IrG;J4^T(lR? z(UOU!D*n({Q@x$f>T!IA6xVVHvEbBVb*Tm^%!D02vu_itGHj9endg5Y@lnuw=|(~6 z0kZG*_g;z;acl83M)jU_jSq^uA%d;_rpOn`YmkeCFKJ)C%373thn2ZsS5kKq?`(>-4jxSLoKa$)vxUex(h=|JPvUW$VrTm7@KBFQp znQ~#l<56Q6(TZN0P`qG_%L!MdagOIV{Mn+f`IvCfQ>M_;$@`AA<~;qv3)@I>+qqFs z)aUdJ5Tw4XVTO=z+#Arv#T`%@pbTdH=j{6tWdm)*gT>e~x3H5PEOk~YeC6LXi#86- zR-^+N=>V)oYNJo`P=QRY#^#PNH>B}wtEER--m@0+n}OZD}(2k?-&S$JANI zMHR5yeg=k;l15sPj*;#VlrBNKJBRKZKtLpwZjcg??i{+i8M?c>FX!BQ&w0P@&-=Ic z^E_*<|4L7_$6_rNF){GCGi~JSG-rD}8gbI@0JU9j3oiibCvDYj6h<@ZlQO&@LQDRJ za{H3fa+dPoj2&e(>@SA@FpM)78fQr8P{NyuS+dRCa^PeplJ>|%d<2JiRf&Zr>@AQR z>BfkDv4wqVwpMj^e@nR*Vu6!hMYorb_9?uSd7*r-zdGLAB&$=M9N$ae1U6XXLA^hQ zrC;ma&v^kuUJ}YxWOT@zN8j^HN7Yx=Gz?;GbHfz^`jXfrol_0ie1KKaIHtZr%o-9< zH5BnnlPK3kN$abo{*B3M-5$9+JrGW-ZRFl|UrtZbqoM7XOBxYwpC-f~Eo#l<7xkY< z{lSw0{l9-G&75fSJnS;MnD4h}Ir)kJ)+4s^BNo?xN@@&g1CdjnYX_nQrHJNFNo4(Q zVIk38YugI{dk(J$0V*G}j|+vLZ-ZC2&Uz{3q@r>!JJ1V5&_AwI;I#D=1k5MN6nU1W zsLi~<@iGf|+m2tgmnQd!N?XCsb9V-rOPn&DU&4&#$<%a2Y-Hv@iJCU}gZ~Lq_ z3h#pW3T`Lj?OIV3V+(ThoKb<&rR)F{vqp8t4YdPw?+J{Uzvx~wzaRJzLmUCi!IK5^ zV_e}f?I2%HiY)b#vi7Y@GiWJ?-_dleRc|TzTBG%MpxfD|4cTgW%t+#5#oALh1cH?UBUKtKY2ZnOF%t$kLX% zwxw!hyW0xI+W!&kFK+$jE@`s}4j7QQAOrNf@!e?v!rZK)1w*$_FXz!6_v4R-rKi{2lTzZh7&0DoP$yo&76sDg7D`EZM4$ z_M1rS>sVL21pZ?EE>1H=^Xi@p^QKL~Ar*WbO&{T&QLxg8%l9mPu#iARxM;34_4CD| zgs;i#x#@1wf<06dD>f#hb-CL8WI5{qTK$Be+bc`fpNdkrgyXpFD8J>X*21FZS+rJ{ zVGjHJNrjjwa_>`Vu4C|@945qBFCq3Zhey61&bEK57+wh#wpVX8G)#R6$r9B9U4JKx zrrknj?m2H-p{e>nqaIu;DUli?u1fpL{oPtIW+MK-1ku?nJFGY>;HwlpiH*FF^w!X%KgQ=`UM?=^nacM6Y&jaoq! zBDR+5FM8tPcc-wsoXZuzb(6IUqc=DDJENJMi@;gaB(Y7uO#<@fH~>XYg2h7L zlimci7_JasyHxs|D|eJRl8w1}(fbb0y{<61Jq~l$rpb`Y#j01Msazs4dLPO=7b7~u zS(mmQMxWamVsA>AH)+||*7{xA0Kb42q^^gYzK;7ld0ou8Q;s6pc5A)IzK* zk+TzTr8ex^1v`~(AGHVX0JF6wxn<1hy;DXCD;?EFz><2>6KRYl1K!cAby>(tz>}4W zDIa_(pA5}t=6&Y?7xnFSL$l_W{Jump{EhLQqG&k*_TN8hdlmB4mDo1Me*pfPM+rsQ z_r}sxd(AUcKSkn#Eyi_(B(=gcHFTfYvTA?U3I9r(97-tkC|Rdo+PRAJQ9{_F(KyA4 z{xzDoY8w^jEIYXJ>2%$m5st6r?9&SI_uX)n}|}ugp$NY*m3={f^u=~ z^{a#DY{$T%cn~~KI7R57!&Ubg-NoeD+LpwTy-C;I(1tcJABoEvKI69G{w-CN3bYzZ z>k9Z{%s;1$W%kx~zqDT+)z0{L;2wr?KNjEMjVJ$vOo@oU$^fQ>;utdcioY~KGNVmG zG1+gE(8KyQRW_G?2FTL9!7qJc>L?AuBX&Z##$z$(3Hr+2 zV6BfdIcVG1VU76q$2{_p-deiEa6a9;K?GJ2xL2){K(h81Ag93TRKU!rK>XMGm35Ip zfOKGU(|CRqWa;O^KO#WlMG|>+K26<`Ob{#c9{Ot;WcS_U@A4n&wJ4FB2!>Vm3>9I_ z59QPH#!g@Fs7#Ka6<_tXs>`z@QzzQi0kw?|-BzZ;r{{vvA_;!$qW2=V`e>Oo8@9?9 zZ$sYI6hfChs|$cy5;7uVT0S&FCuntCeZ`^jHh$25mZ6}|Ed5td}c#J zJ{e7Xos1^TR{w}Z8`BJV5G;@KFRmu2{y9AgmaeJLT z8md?y{k3x;Rkq(bO;f*dje3}#wxTDu9zon+70E4DtBd>!O)CenZs}agz1-pu<#)XK ziP^@XOyReBaUScrD(jy;Oc@K8KYO)x{fxPzksiI=XBS1aJ0hL9z5TN~7*dQ}!4W9L z(Oe3bp5M=VC#xK*sXIWKD&PD_;ndxu<|@IuvK4nuZWw=$X;h4*r^AGeBhMUxM*#V` zqETA+INX<;o*5RsUUABo@4Q=?WgILBc z74JQ`nftH%>bbFO{%WmTtR(X~__+QAh=H*Rh4~Y&ia(%MGzj2kr2cMIfwe$&w0fnsNVi3aYTlbK`X&fjW!Y>Diz&)bZB zk81nuWGx$HE?>bujq*gL7r&-59!=^|mAi$}K)xLBmim1JT5~wAloZ$86B>!iF=x^OvD!}{HUjnbN&?N^7_2A8_nQy)O4!TQ{7ZEf&hREP2@;b# z1Mm6wy7ouJ+YTrmYUqLij6Hl=E*V5c%u_ehv1HzZbMxHac|877@*4YEAt=ABZv|gY z%_mI+b z77n{z%dC6P4=WjV0uG!TKgQLQDL0L@dqVYU{7&?8H9ngRCB7NbKHlhg9^K%T+L@9! zNtYdCK4S<3iT>6coeAt5TG3WckoLJ95N*DF0v$;YxX%#I@2`lI zT=YnQp5Hjc(@Z`hCOB`c$ag!Z!+tVpac48!DU93&nO+qCuRI>xCg^@>30$1vl1R#U zg*LM66MPao*shSG=qJ(nRPt$<^@iPX^KQs<(;oTb;uY_gdOq%Vatj&j1f<4)HAJ)Y zEIVhA0so5V#})V6ZPYG6bp38_zl-1Rcg=96;sN#n&SraGV}p_|(}aq{hVx_+v*gA^ zc$@l-J7|)X@MKIsl5wr;Low`eF;Wy;6*J_Ut;<2~ zKQ*)w64T6)!xBaXGJCA(nUAOO0!S9rprl2ITr2RfL%yY-qtsb4N(kJW1O*n^?+8}W zpiS^`pLI&L%nG0ju!;B}q#0ECE@@u>H1KSCWj`C`dStq!QVm6lh1l<=o^pNIFcrqIil}Dx8!sxwm$(u zdUPpwv?bvTmJ+?u*n2pEg`2P9CGOyXTT;X(_fVcE{WE$-n~^!I367qgv$MGb{N7)c z|67?I9&ZAyJMw5oA_|N;!%QZn_h&bNo{0}u^iFUSRG3Zk|exops3-Ak%>XU-RT@2P?7V0lEI=+-hd@Y(Ef}w&fEM zheyz~$C=n80@RhrtYbs_>-;Ya8@)$~Yulh&&?>Cfse^qI2xjiid;BIz?@7Mi3jy;I z@5uyqb*8b;CIgNuE$+ES3U7(!Q>gwma;$m$Tv zat{vbB+IP2J(8n#6u>*S`iV*L27G1n z%H&orN~2|^pwSCVR;;cWAOh(6<%rz%*EN*nnGeR#d_zi2{4bB>e-E6bk_crTX!OxF zul-78m_pAUtmOQ-&1?HMn5?Y+ZP;bP=hDkubXnc~iCH}Ph$B?t%_4F9qo^64rakKe zT>ieDhqQDaV$sJ*Qz=(D4Y)!@VevTS#LrS%ekk<2=`;N@!E!U_+ca*GGzlDXL+$WJ zp3}Y{M`Y5i%8GZnB|$zLPTIQXEg;}~I<&0xMVrG#_w`ze_Pp49O5BalAG1e(8~(C6 zMU~!U+eMR`+IjbnceYPrk+0pRHfAyp9B^(*V1Dse`@qJgoho2h#yTC8%$(L!1L8Z> zKJJ!cKFY}=?Nh(~3p~l)=H@D;${n*laAVp18r0i&Uyha5^^r^n03cha6^3lbAoHp$ z@$x`R_Qkx+5vb5_y$H$>kD#ULO>QB!H9F+oG#zi~kmc%8NyX3wM4eRcm*^Gq@XDHh zx88-nNF>?MIfmN`q;|U1Bw|8EA4Q+!xJnWyUNcFP!#9hGjeLLHF*C4Rj@r3=pI-}T z2g3LA3Nq0n59cp7($>4bRze591n{TKqbKFFt!CJqNG^JevnicUEUOumBSInE1F5|B zXib~rtb%-XZXrU0j-ayb4?O;FRDW7kS9g-(as)KA?}hQ}gS5qUn58FuXDh-l!i4qS z`{&IDNqS$rf|&6#72KEIHQ?MlzuB7aoxJvHCiiT>2C}1Rt-qIR-MNmwbS@!b3tM{n z&Y!+T8D_qwk$6zyOcALbr?6!o2=8iIxx02TT(MgZIZw)oNO=?zA^UQzOF_yWMK*g` z)xPt?xy`}8uc2jwUq7wrL7d)VqcGwdwXx8q&7(DO@7Em9Japd!_DonhVNZ!nPp^G{ zsNpA>w#S6J%k;2xBOT4g&+B{^&pIdFvp^((cRE7xH0|xz1qJW5cK_f~_p{GF zDDb5tK%KK-U4feX7q16zertECr4E%l{PpGU6e?v%gfp%QKJJy{w$|$^@GkSVrK5|z zAo~_tJaqNuB&%X}CO$z1;aA_Jd^xj{3Wd)AgQeq5O(3kTQB2{MtUCGWz-#&e5 z3F*UgKQQoY-nNk2GHyte)AoC+gppU}n|ZOTZ@71GZZN+>5>LTBwwqifObC<>A4b)F zu&bGcF&xtcL?O# zjQPe4QOgSLqU%0R`3s5EkNK^8L>*{ksz_Q4s77yzthk7kI=c>*;5GIa4Zv16{3qUo zv4Pw{upgzo=Ow8vkn{DaMAg0;gebNUk1{#={cHc(W6fWj5xC(F9OH-9pE~gi?+T0? z8@459673iOgXe(OT%q+o6?8kNI*p|}Is^lSi9|An@a2OzARkxFjE8z6N5Jx1rPK^R zI$kx2-~~=vDM5ViM(LVoc1{n6YxQ{b5uPp`yY>bJ)z5K}`wU zRj?Vt>deldz4V*K`1_Bnw-%H_xG9TU4OZvI=n*sDR&Q6H>|d}P-au)NyTUXkg-`tK zwbPno^gXxoE|bANG`@H9vlE=)G%+j>da>9d4ETKCH3yh`ffqBxN!Q0qFsI8Ma7W{WRaXOd4#T_+^B<2a$o_JrH3M{f<>m=nM zF-%LZ7fQ5)kHu~ibL{NQOei$fzRA&}0P)Fq~iP}>b%xt*TTf_IzzA#&wf zrXSv3n`Mir|ND5~XmmK14+Fl*$-67wSEGnB#{sH>L_8;7 zp)G5@Jg@im3()fnC(cJ~(~mlt8pS&&fjxxYl1HS1Wk+)~*I9ciCh-2>?kIv+ zk%D!B=#07!(+S#>_<}o}4Dm~be5Ki*uCJnn5SMX}Tc~iX-YtYygwCJEOr1SmF7@|I^z}V&VEh?OKW4To^;NzPmwM_zww=Uivd?$kdLg)4b`rKbm zt7Q2NMIF=y0$q>%RY{!R`4@`1Vq%&MHnL;lg>lwSxtjC%UNvx%1-2bOTSZNEW4XDV zze(*$m`oQ$nII3flX;RLUH{D5F2NNdm*y3crn4t%){4=FRFa>1O(|U_a`pMX*j^;{ zxvWeiRvXaVWDn1@Zt1ilfmQcrZg}84UvWroS8y%Eylrd+yh(2KEXuXWUoVyP97%tk z)plK@BmQXFDfpk&^k%Q5i#YJ|lq7L`$Z8XjXd(TX@e0GVAqCVf%>BFFER?h26tT~Q zRmKs5hk{v;cyx(%n?1sYEtvOCOC$G^6ZwrGXjzz+%N!Gl%NH3BlT-7tdsE?~1sery zt7Wy^kRHgA;zSQ0T|E)tUvhvHzBgZAVYsT*?DqK7(x5B|?h2`HYPw)_h`pVCdL)AV zsOiFI8)Yj{Qh3Hpclamf`^@!{0-!R{kB=Z|H;OZInT;0A|Rm+RF zR}QWLwy&FqaaneyRpWzsEJdt2%eI~t;Wuu(QZ+F(3JB&+2=L={1_^jj2Wn(*dP zn2!hjM1^sYxOXl@cdgy$Zc9~Bw)N4wCYZL2Wa&RXwd}Oz7SZM?Z5oGGSFZlA2_O!~ z+|DE#fO#jW_*$!_Z92LITd1K{W>ARLte#0zCDgnobxWMP{0w+n?n2tM4`g1l zbeP^87@?-R$U1oCq!pAy4WBOTFf;#|>Yl@pv1s|EUjc8)BP0+&<_s61WM zT2ZUtwF9#>97^;G5XL`eMe={F&W$7ZxL@@1=-bb*`!sD%zG^#pnV<|hyv4h>qt-?x zv`YWfQdlDzX`DA>^I6Baa?&|$Mc)VjXZp+3dE|LN;5Y%wztU9)d@B!0<+?lT;=p$J zL4IwiVN*>%ODk5uaK#h`7)H>g4z63jPdI*_TINl;*|w+RCo0ZYUZVh0h*s_TF4qNqBhhJv>%f%^tbuw+}BkGP~2t z<0c>@tBR7$>Dk3ekT!kJqaQCnCO?+$?M15a*vsnFC@9VSQr%r=>%yIl-n8(&r=DB* zCLY{lg#TolbZ~MkUw5>t)M**PtCCrU?OQ$mt7#KiT3$Irz?c8&qO*y=83ByjXZ_}= zif5&z9w$|7<1tzES>84`7kyH06qY13eY8ROP+&D0MeZpyHyl-EO*VhzxyaAvEzg{Z|!hjgrqgUJ{j18in_e=#!<5Xv&g*zss4y!Wf_cB|r1e z*9h1PyTa*>=(@rK8$gF%&Bqr(nh;3X((=w$z@m18@Tcs?K8Wk%R4Sc!!6I*5BMQJe z32o>Pw~I0EVTsYhuBi?k8>xk7z~A>m2r)_vgJhpl6-)w|&8Yo-*TYq)Kii$RV^Czm zv2L|0Fe;c0p8zQUyHMfYD0Q!3sZDYBB47m-R>ZtO&P(;Z=S1}QgUDf z2vVlCX6N(o;sTSxGrR8XZ&|BYkE;pwBuC#!k^T*55sP<;7p9E0&pEa$A zJMFl2Bg*CQUeP7%eMInBC`ppnP8yGBq`=b$rnsJGXks9hMsYomm+bOT!V7e2 zntZvaG1)5yXUIPEhNuXZ_FS@ktoF6vC!g;^$J!`qB7!cUSsp8ph9piJ$-l`E^=8DZ z1+QY+e!$Y-6%UE(krAS2;A~H%FLw-L=EMqKWmst;ZRGbNYBE^N#xyiuEwr{Cml*Kj zKNq-b{DBf8QeABW&}!kp-Z;0hHqJ=ox6Sn&AKOY!eZs&(+~N&;I+^SRZ0gjhNqjDH zs-&n)BiHa0N-<9FNhIEzZ%W`CdDs{2mM#5R8jP9u+bNO{ZL5(wI+ivKkq&lkO&p9J@cGk%J zf1Y5IKuPX8QKDW#biarw{OW7>awxw@c-PyK$tBTvjAmZ?2Z!)*j_N{3*nSeP$-Aia z^}k!xcQnAJdOQxMJaO=4h)K>y}~_7j=l zv=+qhL*}P}lPl5+PZlEaVwQhC!6TgCg$@TG0wH-KT!!`HVHtF5=btTAHcDcBZm z`UFZ?hiX5EwFk2!@!yK8zSIdZk6=(=d{2dimF$=MSq}%+jubtwCeJOtKmV$*XsVXF z^_ikw;B3l}oM|Etzcx)M)BlQ`SzPdhcQVsjQ+~k${J(uw{*uFoS6H2SyP`ZvUh|ZIQQ*R&_i2i-Oy@m+Tw zgd7J~|5EL9bN+Vxc;@F@qJ(B1|J{L98YL!f`NI;Uw`+rv|NQcQW}%AJS|6U zB5scZ7%1FX(NccomX_1<=yt+0?qZyXkdCmE4P~SyPgf@19%(Mc(Hc=D8lnRTitoY^^IodY+@JMZje%@QZh?m^DlD}n;4q@T=vGVMd&+F=b zP*RsqBNg@CB8peywY`|h_ioTY@wIDrDE0j$%fkOP^81X+0aT~71)xsX$I-PBtMAs< zLJiuu*4QDo!ipM(q$u1%=da8iFMNlCc&n}k7P^=uaB*_ihaAqoS8J~a1wFe8PRl}1?>a;mf>|r+k#xOPea+R<;5b|0qr_X>au*cko zNb&u$MR#k|m>_>}%VWl4xeKb-;PbmFEc3+yBRE@Kb$ECQQlR&oX?ch3gcm5~dA8YL z^AxkPTw&)loFL>{7gfXu;andsl4#G>ku4|blVvKM!0T|{!cusSMANLFNL3FWL6Ch^ z3OF(Sk)^x${kjavIO_=lv`f?yb= zjN)CwYClmWH?SOx-|SKNMKpR8uJyQh+{vnCdA?UnAx}?skkUJ(sN^JuL_zAKS`#Sw_Ym_XmmB#LgU((>%68U3cZMgrqk|TG~fPtQG zQ4owtu`vXfviUQ;l<5@dJCfEw#SKoJclBPep4{_3MwB{Br1X1_f8zX`dI&^XB&T(e zx`3K;X35pV5i@8u&f&u`R`>!qXLPV**vm67rnQ;TTP13QO}cX5Sn>&$<^OtZS1xed zrZtYMZc!I=^XU2H*PANIOMO zPtSXf@@J^ZD0eo~m+5RXlJ-d*%9ss6YL`TtuYP5RK(D?rFUd190!!P`R26g z5jAHU%Nx>L;>=(F$fODRbEpYQ_zPTZGWt2T#E7OOaj6hOX-4}NO4K$;5viB_N{hF< z*U_a`+h4gOG7Z7z_mH`AM%LlsEHnx^3&l)DdhOYX_%miJB-TBI43NM_L@Rq>62btX zR^tuQr$l{UODk;hxl}v2Hx*_Sg1M*ac}y@Qm8ZNA!g!Tqrr%keH`D%!KzG5hFi#_7 zgbhohEmHGF%%6nnMLnj_SisMZsl{xTp|l%5i3Hul5OL9?F!;mPG>k|iFh18~L_IfH zRB?o3IN8Q?=T1X=u%2wEPNO99v-Emt!VP0jRvyyT8?H_-=k zL=zQRx6Sk7h}Uj?`buLXvKas3XmHMavsmSVQiM|J!=B6^!B6@-o67|-(t z=*FTojG1ki01T`!{wHVfFZ#HKP49*yH4gUOliLfom2Jl9 zK*W9x@i!gS0*>gFGUU&D8cui-Z2avho@;s5EI6Mwm7Y!SpH^O;VD99@feL7kFM0@% zQQGdW(=Ye4e%GFf$LGI5Ytn8NQCSU#L9*O;;@mA=kg%VkH)@q=XS&<5l7aU7t^T=! zm>A{r$OOBKQ^wkcRilrO9CD+Uke&H4M2GWg2H(_DKorO>okb8`T3EK$gDXZb>1wc0 zxO60hmJMLb9U!SY6t}smGh2 z-wZdhm1|@=jCkHxuc|BCP;@-qEG3IP?MVSUZNqb- zHMkhtDG2kmiY#=?@nPil7$!LqRwsR8jiCxA-XZ)m35oBOv{WVic-Ep0N*AyYY;R0L zBNJZhqRuX2(*PmfU@Ryxyb*DkH*X}kaE!wJ5+m16_m`_NZ`uQtX{`|RVXgwR(~7y5 zALe3&`=Ga&0sMBIg|-}-xj5{hG}lwnNQzt~3=#ElLEIHQ(-8!~8h0!0vRhCj>&K7X zVUIpu3$4R(C(-N&e3L`8nU%eAD88ndI&N?87u;@e6m{kZ>UK`BeCdzGQV;oInm>$+V1222`st_84s!kNumA6?m`D9u%VVjJ$7- ziFNJ|%_?la!f0|klFfWr+?fDBuY>7;iJ79$8=_Is(7NxGukFUs)#s1qr4vudnRm&j zosW_(bC)pp)VMM&8USA*Ax?VT?g*YqzBaqJ?VEME zKWrxT2S^@M+|a=8IgbwA5=GteEx$&FM??Gjrt-a z=zB7~hR+N+?*X1;d?9)tT)p&M=NH4`&ja-?>fZCxuM8o#br%IPolSffcKytLmw>v# zOd%g_nQ;NqtD&EmMo@`Nj_GF-b3-!RbS!md37w6Z#=R*fD8C{@_w8jv3`E*WVbD}! zHme%)`Haj#sW2k+n{yLiXkH|W?U-aSN#O3^h*JJuRaxF(fntaPcwhPc4~jiXJ(X&4 zge2E)zlr5|oM4RBbBW;kUo0xYj*wy;Thk2BYft!3WRjTXp~0b!c)7jv`TEY-#f_`1RGuHDM;sTku0C~Il%=eD^5fx)DiiZ z-q`O|d*p2Q*Rk6EqKT75t)d5QKwD=G&H-XI;k7<1*++ZMqZ#sv(xk6LDrYZ-J;GEJ zLhbZA9J^80CR>-8vSvKJx}=STm*}~v4EBBaSgg_hr8XvBY#_RF)}tSKMetcan)mF(QbxQ1Id2J-=#`fPRWB>BQ$&Y=M57^}*-gowoW0>YiI?*75mk$y;fi9N&6?Ik=jZ)qma_!{f`rwbGLX0VQA~T ze0&8>+vb`@CYeoT=#w9QXpbu&iBu990_GC(H$Z?{+|l_93dT$6Qz%FQSy{RQMOLH>wdB3`Z9YbW=JqWyVCYjBL~~ z&jr_xvlozo>b%0ngv4I=eiIW*n}J5s1EYrP)`w#m_eCe?Y4G*>^!r%q zWuhWt;nom-tlKb6Eu{74=;!)IkB|5g#jk}MJll+1&A7%sF}!to>phjZDC8HL5#RIz z#+X`8jh(PISuW-9XaD91>1a^Q+#wbI96eNil^$)J#px_`IMX{Ma&ZfLgz6LL4f*Oz z{`$Ft)3RbSJl4Z5C*;6iz69#<{`5T!qV$<0KsWT?((t3*B`{{YYr-fszZ{pMRHz?9 zQ-%IvoX%U`{6K$(6T!K3dO4|NP~&5lVt9-Fyo!zRA(d?fi3)7YhRP+qQqzK zA$66vZl0yaX($J=QBIZ^u=`?I%G9Q86?@4q(E~=;#LCFMGJ0<_9r#kxv*gyBqwN~T zfD>1==KKBP)iw=RxPF{MADW;fldGBU-QO{P~D0A0>8r=J67&(3%$Nu(KmwdWM$# z23hh03mvwBMb&(!h_k@cJyC`N?BYaCI;Ef^a@&4mlu_|pucse*CLmfw@YM zS}9%T_%kqeNt$emPL(I6Je5|oD!KLrQ~{mt${4*#*n-rzVO?8zR5$AZM?E zBJ4RdI{)RilTsS{Oe?K}{%9tB4rZLAT{FmD{zl2#R-$vnnI13^dsH2Hq@sA~O8;!G z!{?<1IU7Kb0-0Q&Kh+&=(uPhBYqBSGAU{rgsO^TXi>QN)qDeMlTB^P+?z8q5Rov7H+mv*NR z!9Uw#-n8!BQe+^vcogbQ8g5u$SpTXPRy9=fzGu

    nTSrWFS}kC=!@<*N@}xW{a|6^pqHTFa*Qi1r7IQz!0XWEo`WUosvVdhGKy! zr|)c$=#$My&X(@{XsQr-!}jP7hmm<1(fmNB2rS$+NL;cD-l8WyJAx7mg>K`@uTo3B ztrKp!%z^fBj`pnNaPj3(F=1szDV45ovVp_hAnbAQ*Q|iP{YKJW=G~BmaaH^mq$ILe z7>vfUY?|B)5}&NbTN|&n;?q*eS)Y7C#pQ?P8)Qrvs6zBy(Wjy(W38Yql8a{j zCiKzSYwx#~e%!RD!Ysh2Icw|HK{i-sLMVmTZqcww@2QD*66`kRJ;1WFR^8!X<+;Z* zV&~RGN!OQ&6D!xrx04~&RDEntk+q^3odS_F7uaT+D791hxKfFV>cII9kQ0gqEf`Y| zN&L>n!(CChb8eHo6;y0uN5sOLIvhy z?A{m07^B7FrZtCSSf|C&ye-%Rj&XV<%3?au?Gk=$=qgK?|6~2q2-L|PqoB^kfnffS z@c#HNYf14T1R*P;PJRA#lVf4m`Fex7M!WGlb1Cx2{O@I$iMLk( zUZu#(R6Jnj2rycs)}9ag6^XaJg6J!>rHnyruN032(5cbjlh< zO(#1a`?(VYb-M<-m$(TCeYXIKL3ADnj^9an$N-Gi#Ut6F+kUQAY|ujlsB=QayrSo@ z5vZ4<@8i@2I^Y_LaxRPAa7Xi#9Ym#m{QUj9`gO&|Nd3nGpx9{X??k`JXHP9bvnZri zv}Iv&xXQI3>Ct1`!*XKA^GsPmneP$l9#-{~MsSh;fl0SWGzp||g{~za>6i!_)@-#`TLcJ2D zjQVexTOr``BKSF4wdI{i^K1q?SH6p(?qQFd*&}Kqqn>4)iJCZvlfX=Fw-xk%bJ;ru} z`U2Bmw(O)gx(m<0?a~Ld;ePsM3bgh#dV~BFaTV>gy3F35JgEAEEwdABlT#uv{iaCg z2<~le|JS_tX$wn$u=jOtUGo-e)Iq}WtK&aY@un7l^tfSwE!7}l)^xl*=t(^)%q}yv zLmPAt zG;9;BbxhBZg)i&M^6q3+ztITOaks+nEF;PT8>^7B3Y8#T2V4f_f75d>tf6dBIZJ-e zsQ##8!|}XBLbo?)_v+KKfArjBxyqoliTyln2s(*(de6~%aNT%oT0`0XFD6;I;_}_o zU1WDOY(YVQL7`$_p3Jwch%v(0hWokX{Q+a+9gFz=M?s zA#-mEi|x6jE{BFj6n1m&4K-Zs1t%)4kJ!*IHM;BESKPn|C>y=AfL5j}0nu~nK2&vX z>y6>p@2mkv3tI)4qgd95M;En<|4g$Lq>M8c;|Z5(48??Se9slu}!6jHKH zx$)8Zr_W|dpIXgU<6rRZT&whGvzGe9PTGxYQoZ8WK<&qm`8K|bZ|oK?mBmJ`{we}K z#9Za@C?92J4GK&Z`86pg+_9>*P1OxP+(Z&p-~S@|WOf#jgIgxXjB0-u2CKgMFPh*M1c? zZ2lW|F!*Yr)+WBNxjE-3X6*PyBQ;@iTT#QhSe9L3Ch`Mz^=|Q8$^g^Os@QObMI;t~ z7QvxRv|LYH;i9CmprbZH;+=60p~rgXE;B%ep9N<@qxmmu%S0ExX(*f|CTE1e3s)pA4nn}R?V zr4Q12+#EF}fDF!_-;7MFD=>erD)KNok}+y1Nxb zQjzY?p*x2LNhPI`?i@Nr0m-3ZhEC~@Aus3Nd%oxV1Mg4!d7riSUVDAa(5k4x4uk7P zH^dM;j^;d}enH;J;+atK3+oCFj~I%eo^IJIG&>TAsE`0r-kipmsAL93XNlc9c z92uPC2@;;K7#h;a-1T$f;xe9@xdk=2NJ@l#zZQYxFDvIMQc65UdlYe6(MBn-$?Nz} zd5ckLjx6`o^FbQEd-FKE(+h5ElOw;yKNlSg8eufHGEQu|`0vm`7YBOT0t^X4^af&G z0z01Y`=F58E}trxO=aZW=Y6lwa+_7&eBI-%T^?^`>ed&eG>R<}sJ@BJDOfyQNdD}r z1U?q-b_PbBle4V16QfGhSlc;E$bf+^c7Pf9s}5}q+lSoyO_pwsS`x=yP6t=>$CI>0dXZbeyQl0)es&OJHQmB+^Q*}3DN&x2H1!ZF+G2Dlu9 zlVkwkOeMdS($|(;w3w9wFlY6eqQ60MhLsZ z4jiWH%oXgXrgCkTnvaLzpAKv_MD>&mfDMZ@D)Wp{Qf7R*w$0vQBHYX`j_pea*&?!oPRceAIfn;HKC>2^XYuBvL)(h-XGsSSpALy<_~4<;@ibbd?#Tr77bXVIcAd4{S-Vb!=bzV*bZXetU9zoHIisd$e&P$l8Zbhh- z9cCctuiJ55Elqr_xY!T)ox+DHE%H{=P&mHL<@En z%@<*ufNHYZQ^Qpz8&e8y+`|(Er7g!BHae>!lAfW4-n4iM=QEuwA_p>_u z%+40=_mET#c2C*XAdW)1kUD{F_GZApNXe|_(P+*1fwpYER)1?m~R8)N;ujVP%J`u$A8rB8gr&Gajl?9JbJbbJ%1I8Va^ zqfHAIcTRL3&Z7RhB5{6@M`qfpVEH>K0lJYaZFU6D>9+>ylf9uBNc+HA%hh}^)cxRN zrT@bktzbI6|DML~n{K=y+Q$d(-;3&4!7b^MLjnwSZ%=&<^tMqW4{)tn{qmZ*8o_kv zjWO&KW^y9KV;7eEP3M64ztOwyTbq{g6o2@Ecq>$TojzYG?0O+@@(qcoTKH?4R4G3Y zyX9jM6MN*nIOs7<3j8PrEB~?l2O6XDA-nUTEcCYh_HO>g_=^e~^tic|vdll+A26l+ z;J!C9Rr%o#+I-4O!!{?RYb>ftePTVl0IyfglBTKBVrCl|bFcq>bpF_c**VDD25_g& zUPZ|TtZJekBXb)C@l_lIIuTwn30wjVNY**qPh@?6z8E2N920shY9iF42sHw%-h5BC zRwVH+6_E;${3-z0hFzi99wVd@9eth%3YVH!vaL9C)S4F?$;Mx(D};8n)ng87%6+Y} z7bvv@r*nNM!9eidrqPUXp@M>>%P5C$1s(dJWL6l19id6TMP2-6!j-CpL1X}?yzGsF z;X)=On@!6f3B#vXtr$L~Na)neYIF6^iT*$V@4Qp{iyyW1W~`e`cwk*>9BKpYQ_FTx zx-}Uyg=rBkcafwV+u<&HmZw<1|Mrw(mte;$Z@5l^IXd?~tHqkvD?H~5w0m29oUdOU zb=S*>YpLv9$mOIq#tEfo2_wg_tm z!^{_~BvYDQzWQ$nEk2|=7)3pYbPJNuH4~Vp6ns0%Z1Hk1s!^BT_L5aI`KJKRhbY8V z(5BrPUUhpacF#9HuPb;CH95@nPXF9C0~>!8tj^chkf9b$vS*BZj)Z8d7avxwYS7HE zli}4^E+UQhyd~i8mEM+e4WB=o!ZX|p?H?xV0?_x<$a;5IgfQR5h=gAz3o6bDlH@%$ zPdrVgbrI_xl*m&A>5Cx15|*L4NN}^BXU+5Out$m+>*CsL<^};j`@_X~gCVZ`;uqg9 z^Mv<&b!;`6)WlhqvOnCSHb^f&7FET`;u-6r%@UqkIn4p+C2YvkJ~4PU;{zB)w8&IS ztE;BJhLC<1s_sPB@|${{ly6d3gg3ei!M~~#dWR4l*(~+}6*ud~yvh)U#-myZx;N*m z7%+ueYq`rJSla}K01!I>o0YIuV}obhJzTqqSCOM0hlV&@1SPthvE5ID>%Psuvn3>e zndPSnN}IOCE3gk~=4iEZJ5w!iG>mkibuL=7$_CT0>s?7$`hep2F~Pgv$_AogVQ+~) ztf31EOoe=sm;})l#8Q`Pv8S5N&`kCNDklpZdp%&=Kig}r0Hi|z-IM4(^e7b09oG+U z03!Ty3uxVG0CTrs9hxp;flw>oEJBl;K6(9u3g~Pa{|F+K zm9LFn{h7kyDJ=cv5bWn=te5fQ-O*J}rQamQLyv2>5$acthO0V(#vIQ5aWzwH9^vm> z%0C8CIQ;xZy9y+k=7!KkWr`_}ll2JzzNz(BW&JpI9chZklm}}1y8}24H!U7iCY}H@ zCbd}=xOL9InWB7Q@LtchHzAoj9D#0ZcQCOR;hE-AktM<3cUnJ^nR(gj!Y zzSkLi&nwYW=2V-+%yUV&y1jfY_LNk;F;wz~bS3DV?K8Er%(YW7B}4Yhy$gzSzm>$2 z&CBvH>`6sa>EDm}5;Xl$vVG(5T%ON`os#W8B*{+qk_)MDG_UeEOHMT#gRM{c$ZPR^ z4P5kEK)bD01GAUd38#@-`f#C&I1)Ph%sVeEI|8(rKx{gad8}U)0wIo#ya{xrEeKx2 zlbsTXOmAw&m=HEoO@?N$(E{d-j(ktKVwlf`yn4v08c_(SQh+S6=DPx%bAs$dZ%Dvs zv>>j!N6QH%;&XL6=tE!L9GVpRewU}5E~9^>@es^6`5Q0W+VRJ^`?;ZKB28d{ipgvh z`XX7PypN(%#{Vz<5%JdfkZdsC6EyI?s=>#B?r~E)k}qs7tUy6S^s9)h%bli=V(H^v z2GVgy*YBy!J+nfEJO$p1P+-5J`GNukSz47u`L6RjkPidWALG=f1N6%5I*KTco+9o6vergb+aFTAUl9Q4S3fIzPYH(|0c@J;5YtsN_!(=k;4{>W->gO& z1br)#9zp?zhgi1BOET>TtOh0HS)63!*{nOK?Y?^>Wv+IC)g#UhC%sTm6P!h8}kls5P- z+C_d`buD=;+gReL%C09pxJ|6@hG5ZrHfjqC$NA{#m^XU{96r)7ABgkH!6Tn4ha$uS zE+2nSsU;IgJ-ld~tPnStW^nfsN?q@=Lm|_5V;P5|`~!ko<70P?Qw8%=q`R*t#?WVv ze;Uq+L+p+6h*nCvGuTQMXJXC$kgD6im>qisPexW*?IZoCAxHF1yRi%_E`pjWC;(bN zYy^T&O;!z=A4e^>k553O=nu)P2|fM68#@q6GXQd`?$8i)Tw!mAoQVn5C`@&Q_iN`B zBKZ6E&Lj@Jnor74(u4%{!tgiGt>8tUS(po4IL0$7w-z_v^V2Q?@k^-=9!9g_gUt9y8gVz{od_6Oa8tNaH-+ z;<>_ROZJ5f!gnYuLv8e=i3*Fa=K3=$AK&Y3&*=UYdZJ}VfJUzDw#pAHp|`)`QEvW; zX&5^lJ}P>F1fv0W>d76&UC6+_`$FJx>-=@&1Wb`O>dueAgoy@*L+wYV+LwydVm|R& z?S4W$<()(}EI(2DKh1RCY6sbH-#uh#nj`nSANNbGG!w+?joz}B;3vl-pE=44Cx&ke8svksILx~^;$cJg@R?0R?$^f1~F?ofnuD+ zUG=jLrc`u9DlbeFZqEn_KA<2;MPhQJ2)MP>n&Zo^ha_m4k7uWwr9T}q`N*MJv=Te8bikf~N} zg5fdu#`4nyY&ZZOehE?ORGS|kB!xUXP&#Q3dXn5GF(M^yGI}wNO9HpY*9p9f3_5+G z;sLv%9vw%SIM{__Uu~YE_ZwQqs3{ymm_T`dwL=$YcXyIFcmm(pRYgiOtC+%9Jld$3?x$Pz<|nFHxbBHum0!Y4nbs zM@;GEDJ9c<9G!F(^y<)#Nk4*RjmM{MV=2tV1-9V*(ACS%$>kc<$CfKS(c_Z1YmMR} zA@2aG%Vnnn>*X<73R3eiAUN2x(0uS?NKD$%VUyA1rAZkj z2N3`8-7HY-_D{sO5*v|S}M-0S3 z_`c2RWfS*v680a;zQ<|L)`q5FFH}Gcg@(<=rm=}iibT4`qp#Y@bo1IpBy!+Q=|fX| zq{M}{^o9)-!Y09HGgq;6{SaISk(5w8dpSUsU~<>)yl!z%mN|bG=RRPoM}~U-evP`hFVFmF0vrMzVh0iee8} zdWzafNZE?hU=^q`@1WO+o;lX*T1y9!M(=zp9`nu zu%qwdN1y>3!cOioIj@2xxSB%L7SkWq9|(<0OtC{fknkW0BsEx16;j_2mPl46@drxp zPDz6QUCM|8EjG0>ro$_x0m>;DXx}*hfWnH*oTIhTZ*4UBXUoKKRAoXY!IOK!^JcfJs_(9CgOBO9CN zXu1qD?>Qowek_h7PG<7E*cy&f%_|!V#83q9(afzY2o-rRcPHM_9POEE4w*|$B3o$- zF_63iSqt9naypQ$whnvvcvMxI`mV}z9|rD_2YJoyTlVox;)K)(w@^iL(Q02&1y!R9 zbcFb9ISvpAi^4bj=3bnj3U_rA*f-Z}V z?vF~C0<&_O(DyJg?kp*Jk*AcjE!xYMy*QF1uCl!ePw{d@ENuK3^>9ISg#E_#lyGsX z4jX~xqCdzzp96Ud`@+h9u&mbe8z?!FOs4pQ9(k{-+>G_;k^ldSCL5JT1Z)p~FnOp(@KR=q>r> zG~X2NZu@0^g?)qK<_jBhzuAJ=?N_dwUV_PJB%#AC1k}))p}6|zemQ(=|1khyr;M8? z6~X!(J5NFI=IT?=fPxCAU=_p0RP;4-a;CD3EvEBOe}!p+Mubsg7eK%7#hF&YkGb30 z>W6x`z69}s={dy|$U%S08pEXA-@j*}B#Yn$hPX2>xF+QVeA%!&W@V|U-t$hhy0?}s zM^i5|jvp(5J_d&Vhaxy%^=fF)cE$utWG;#$kbaEQop- z07Gn!L!-(n$!MNkxh4#$6!SdB3^CJ#h=E79q?R5lDp(Qi49Y0`0cLJ**Mh3=*6YQK z@y4)638oBh&r1|3wEkMsM6MCSP1-!uWtn{m6DJm-#d z?52r-zijUEtlKC?VIGyBkd2V^mZtrUO=v;%L(fEpp%~Q-F;%X+DCsClzjZ3@ziepidmm=>44Kq)u#c#(A{M_50n!+U}e zn+N~!W+wTunLo=ubQ`0|sm-&NW7R!}9Q5&<;nCU-W)6xTn~<$iyQPXSYAc8ED2iU7 zA8B1zcQ6IPS}cag{T%GLnHru9p48}8R9d>e`K~`);`E#Cnt-^mfY)^8jlI-Mrz6DV zq;1UhZO(7_xyoD8@PmM7`ct&R67Pc`>vEkI|18g<8 z3)-d3+t2vk7>Vy7AtO5^2a;luakaU@wUS-KrD z-M}aY&WqJChO;IzRJo?ok(Xx_hHVyFcLwW)I65r-p2LiJ9v>P&Mr!TS{Te7WV=8mR zy(((v<{h6E1ls{=hK=ENY61o1YaeE);L=71S{mJ4bC2tkCcvPpX3i#mqR4-%!T@0m zGTUIh1ymPvK}Fq26XQ)|^>Wry&as2;40GPzx*HoN2|DeJA$sscrHfKz({76!liDYHoR<>zZ8>;v{{_>=&&B2Hixd1~m6@}Q zkQ*Vn656*_)T$qo^sKpaAf4)vogkHLW zZ9Dyu3Ds#V+l$=W^B|)aJqxdtIkF0{Ne(OZ0@OvCTGI9Cl3Dqo1rEH3*(+Z;6@EQK z!Bi~x%)IXnX7)O{#lwX&URO8Jt-Ct#B%JQa_bbAWnylPC}#Mi;yF#L zksvEQ*p}4)rfp?Z2L7<%KX2jqu@c^>*g~s3yj@rMmKKxmUBcbW*}`N)?(8|zP*L9K z5x))g-+C*d5OsTJhahOjYt|32h(chkZeGC}GGf$=BYVzcG(BsTZSOnzIO;T~S?%$O z;@_?uMpKKE4gZgsoav_e6MmI;n+W>Zz#5P?^s55otJZM&p?S@KW03#a(0Tqc$voAT zW>Sbvo-YLN@RPXu8?*@V^4uGhU+6r$m3BbH&3 z4po+pdQjI|w2#PBdSe?u*~Ls!j9{_MH?#xPdayF!AT??%(`Py*6p_B2EzGEU!X zxFn}X9Q|rWwZ??f52#*YjmoUCT`uuT#=Jx2aaQu^Z{iNcmes`kxUkg3;@iG}97)eL zbBsU%MOb?;DMgUbOZomnybU?sTIyKK?kEdYx^zDWin~4S?yue3yJzOPH{R6kv4y-6 zH0@s+n86guyMAa2P!5fO&Q$kT+cmfPgWrX06fJq>qN`Y>?4w)A+F zTFEFbgSMzEFRbnsui?pcvsM@DvPF=zv7sKDweXExP$MN^hA7E=i;9`0;g$oPpAYcI z2$#<~IYv%6X;Wz$48 zbbO6E`3vP$Z7OVzzob}v*nMlb9=f7pD)QYUA`d4U_5TUBlV?`zPT(&w?h`+2$3hI;{Re(>n;zC#UWrx)s1u|K92tqpgV4#})A z&}rxgpzc%rT~|BjeUna1g-e@9KU}6sl83D<@qLb@+|JhJ5ETHI(Rh;>r=^Y1-}Q3QI5hawbxU&5VRyyz;!1y{Uhqc@{*pb zSL<^l6x!``dZ2wG=eV#zE~@$C`)Ib6DFOGfc)@IIz4j^+kHxatETi5^wOGK=?M%&} z;}l1VZ`6F5lz~qDxE@V2-bbT(2Injc$oK{lJ;^Et+K`d1)E1`s686(#-@*Z8!Q$M1`2&FTd02 zRMwPuY%!)&%V<=@mDU_Icu%p=;;-K?LzCUPU-mp#BYpd-FH_-)KnDLYxIp%EW9T%> z_g{Q0F_)`M`!3*;s+#P|3|XOse4%%JtZ~>mh0SV3SQeIqn{vE|qC>${joNY?=6)G8 zj4-Y>sKV{XX@&r|D3sJmixD2;;|P)lan8HH!fOig$hg)4*!~*(Mx+; z9921`eG9MS1sV!vE-k@Fzl~IgrGMdmSqb*GG+SCGu|(O1Y1oI4Z9`O14X5SHpm zWA$&`q!asMb=X~MG@a6NkX8TOm7u_*+L@{M^;O~ckt(=@+%TBt1U=FXx%1fl~q54%{cU!79d%tyU9%P4r&2{ z)2#a7%p7wg?Z6TFmJAXOt}Jz#v7}@){W||pyBYxH42$om57^zdU;qOhWX7GdQ^~=3 zh7XxD6GY2RELIWnd%AR+T(`Q5`5D|o@tC(Sbw^@C!J{9--{S*8_rmUsk3@@4GwCgt zDayU*NI-AK|C}=XN#K2LmfV)T)16%=IyBC)ny9+0lV&~G1aqY>vYo+_Y$3+Ha)Zqk z0SkCVk|;#)N}tkUW2M3(MuYe4Ge;{zzmX~HfJR|}4lLDt+-o9vP(d0&$-hprR6Jr= zx_V|G3pkdoX&`7D@%GmCQ(6;I>y{xK5_5HROA*VRc!JrdC*0%PY|v^J;Wls3Ie|>o z_gA^=gJwMyldh7Lt-G;2a6Pn+y%ZC03Z5C|*44V}f!Xo0z8vD?HM!jGo|HyjB7>0&oy(dON{J2{1HlKjaa%CbJq|N*O)dYrh_HOF(G>4z~f5 zgzh?q`!WWM?bKq9DbQ~?zY+H}q09*>ryy~OAl;!!W zCAvNS6-gvpA3;D`>HkF%(K7mKl9u6Yk()t4@w)77cd+07CK$uEZ0ceoeLQe9uxpFP z0kLQJUt^O0JHl0v1I5I|L{e2Dp4@`Yw?~(e=QS>kj;x!lWy<3i3UN>HE?WIoYjA^+ ztl^G<(f4Ui*45ES>ZYT7A4{2Eol}^UzuxI&(Ua)+&ohA_s4vBAPzFxD^b(xW6zXj7 zzRjUj9(`wlc{Iesr$6!6+6^iqb5L_Juw zmn!AwPdNi!pjqbiMdK-R1aOxo`a+s({bOD=P3;=aJ3aRba z+kdI&&OH$p?aUIyF z9WSx%d$YB)hot7T@!_xxy|F@LfJh2ymZ72P{-V_L>o*4_g{j;&eKkB1&AZti)#q1z zN=I?zMua!x@i%&LOPz$(ia6NJl-{lKg-3?(mVMgBJDRHfp@}L-)Mx2NZ2}g9&7B8d zYBgWGMh%Eo_iXx5Orb%~_zawu^tq0!!6^jn&Q#pCG_MXrqa@uWvU&&5cK(0`tCqT4 z3hud{R%ySFr!&~kAkA$Gsbj_i9?!RaElhoTRcRrfym5M8)~$_n2&`qDed1+!e{Y7a zKIhu%#@))2AINUkm7>^QQ~u{Iy&K;^h^|Jih_ubITTvx3uN23)R0T^7aH46e+otL+ zF8oW^?SrC-Ek8Z@>iGMcs<0*}m5Ilq!-~ezw^I2%oxI0$$J+m1|3?y*eprorcvwE4 zn$mT+h%)g$gf868+skm+N0u+N%?j9~TtEI5;t9ITye!fc{!%EpSsUk998KTzCQrdo z$ApsZy@_yq@kbNquzrsZFzlq_Yi>F6&*s=kaxGt9&e)2Y_^KriX!aD&emW_84(TBD zMEy0rfIlxS_e?>7NGbFnzn<867I0J{0=By7#lRnzglLvIQ(G-< z!6SWSP2BIJ{NJ>N6nne^A6AL6)!CClZj(Jtjr+0zQkv_GAH_L5qF?9=U z`xVziZ{fO2)w|cTeZ2fc3;pPD^F)#&0&KDL-1*1_#pgUV*bfsvoe?dnXIFCfA5XPr zBf2pga6-KQ2Qx>M=87fJ_2Mb+GGA+$qBToZk-DBw6#5E1zM zU5SbVW#DQ?{f~cFmVhYyv%UMX9JRZM0^7Iu3)XM1&mTtrS~5+f`sb}3HT`Fx_nYbD zVsLO`Y)sY3(ebF(pv#}J@Oh%-+AB_v!9Tc~!lvEP2-kZ$YA~?q9HhOeWZ2Af6gg^; z^?8B?AHtq-n6$9oU>%<#`wcr(FW?6|4S&lVet?}8JsAs`>@YB*3HZqla7mx3VtBIm z65-3M-p>5SCGC#hg+8CN-;^v{nWZ z_5^eB;FM(dKhdaOyg{M)l3-OpR$+-o01N5rurL+EKN+Jy{QM24?25x=ay=D$vU63@ zmY=NgEKl)7!+pil!=T+`OuApH=n;;VzA@7_?Q8S%vZ|aptyo$QFpFZ;hjJvY44Ot) zpHdYpVlWm8xP%Rwxqpkqx`T2IlHk2up8)t?xitg3+vzr(4BIuEQptwr6~W*Mt8(fw zIS>I84;jR*dY8-J=S^rajfO)tyv3`el9MBa(k8$-U|+Dr!_?k)*M9`99}ss7{Dt#Dv_(5?X$#mpYTDi+o^-^>e*wuyaJgT*Ff~Ui z`;SC3HCU=<%N<2pT3S}!-#%S<@flYT@2B$xoXz&GWXmDX=8QNIfa~tZxu*t*KyY~| z0@S>*A{P;&Z;fJ!8K0Nm>GXZ{gE{MaWb%521ZT4wg-QS6JfbxTN{|WPao8SLdww_Aq|aM3L)wEQbg3mii70rW38O{o8r9RY|4L zAN|j5@KT4L7=(Uc{O2e(on`*TjEjRYT5Q;CUvM5n%`ekWnFt#apnRLEt6ZA*@Z8!i z5i%rZ6i*z}VYQLA_AO*)#rD{_Ub(eca*7 zX;U#?l6((H_->Qhh=4N9IKx6YZ>vElkA-;BhIwtJk4XLfc`ZbXunqJ;BR_IyCoK`8 z)wZV6qvT_=XKF@Z1u6#=JL30kuv4CuNB<%O_$JuWr_aQtpUtsbplF25t=u~zr~gi^ zO~sjIPzrb9QlRdp1Z*sy!l$YQ0h@{1Pxda9SGR~`fL+4SbGG6yK@UMIIvQ<%)cGtk zw|R27n_+NS8TH26Z;bEZNhP&0TQXL=jv_&8Soo@|F!Ter%W|Gyi>+vMhM85A)BO{J zIq4K!4fFewF1K~O_$`;pNuP$QKt^cOzTz-44g>4zs-MwIn}LtNm34n-t!;OXUYMuH zS#RK;FjHtP1?2Z6bk<+Zezh13mDoOs0(1u5NOkLwt^}lgp{<8mX{;uekZ$EK3K-fH z2O#^8pFSb8gD%^*8l;Y7LQpMdG3*3(d!2UGxerCr&ekS_4%JivHGkOWAmTw+{rfW= z{MrmZk6_@JMF@gr%+*)g!%+GQa3Eb=$3~NV$6906WoS)kK$|-r2JeN zIo#Y)XkPYi(KSO>xc-0XLM>2Dnro+ll<$>wU@`m2+&%Ko&Tv}y{Z@|A%gt8<&xl|D zMkv!*y}CftIm$zmmx=TNn&2m=n=9t$sgz5&=PRk{ zvIl(Q`bXI4OC6CQ0GWDWaz&i^_o}%zWiyOT#uUPF5!U|&Wf~S4MY;Szy?wW0+ysTm z_8q_ZhrYPlLk&u}@O!^>eDVXKr#Ju%Dcwh5r!;yl@6+0kATowj;LR_n ziBkKs!vvid>NWd^vIqd3U;XS9D4$EL6WB8swGmeQSl2){bli1+d4|z_&Fl7@a-_4h z7U%j((xRk-XUD3){s&6;UAr-t)G1N!-#$a^&ox0RYy|!y)~j`6R^8z49}F*<2NFfr zvhRB#TU0x^uOFt3oj)so3~wqHD%9j<)ElN`ZCpvU8E!_i*ZWFQP8p3h;FhJC`KFmonXr#q6+IrDwg`72lOT(i3zAx02c>oEr84&M~y;_xhz|&KR%*jIU_v0$6)yjRj$s>=9-vJVoGAkt*kj zKhOwI7tb~Eb%BPMvRgrqTzE*x+R(pQy9Kz6U#Wh%Zq_3T1YbB$rn~8ii9-Kb^|SQl zS78O?R4fs4oQR^QRD320~U_G`#$`-cTicR5%`0;)RJ01YVQUq{OW?Jq6& z=`ruJmbYEHm4@xNb_tB$>@0{w|L#9%8m-i9keEKiH4hQ(s1+zmyxjqO?Q6Ms`9)P_ z+@<6A-K*@JNKzWi+0^0wrA*7MtIPT^dfbZR#jm>2S50D)sHdNpC8s+)HauH9+q49M z8|H(KM6jB6{sAjukIT2x9+XKa`9Ix0Xr%s$%RbxwPB{>N^0?YZ$}(+=o=-e#ENx#Q z-?D>2DEW2Umn!dp^66iYN_R@I|F%q+_p+=rNuJ!-CQ4y+nNEJ;n)RnNc7(cTD1E~K zTT^>_&Cd)H{1qOIAC+-%TClO=|M!Q?d-=N{Jq)WZ?G`Y?hg9Mr@yvmMc@2+ z^?c6=^L}7xR!n!KE{8twE#Y|=51Sw58=EiU6=qu9{lz)9a+^!N-gDwwq|@GZ{U1BQ zL^8nXuU}G960lyo#z?{!xwYWBHDoEN9sgzSXC;&Pinsh5i6dQCklqyiSp`k*rU_`@Ysz_T*_Ns{%pQ>Eu{ zlYLdIHUf_&kBbieI2eh;yh=zMIh!PWfk0bi5~<}KU!QxhMhe`{pT2&=WT1qF01;js(lkR*?`k}>Hi-44uJul6gJ!2Si8RK#HxXU1L zYmsR2F!j??P=1O;50x}ue0naxdw`owxTjG16_zT?$ye@pybJMdpdSDX&Jvo(+-9y% z;)BdhoNtY^;*^#6^o2=FS+OfvZd@TOJx+afI|C{z&EsoBUxiRSs2n(Z0sBF4mFfxC zzEkD^KeuNbABe`SG@{dFgqo=L5U;0PLcgyn&DKlEYZ$@iZO|if*xGvPYU@~^C7ro% zQ$5KAb{aPu5DWLzJ|^FQiH41ttNGU&_}v}4KHSF5OW;gqS!40%X|2?`RPPn5`X0{b zN(FHe_%U0ES_EFQ$jgCV^TN^DG^)$Kx!3~@Duz{%#Riy4Kal}E=XviAWikpGb4AA{ zHq?EZJ9-2zM;wMOMYbee5No-fbKufTc>8sWp#QB=4k9i=2fdH zJyFZ;&~!?LgyFDbOCA>SErtGMCEFXyeCk8_8HjECs_p05iG(ukIa$h2`fP?WxtS#q zrj9E_qcEeRdY6O5IfOZaZ7VAKmPYRxI0Z9k>u3*Ch{nM}jC|*GkqnTLL)-~6Vmm#? zXlb_+CWo@ex$%E#cNvM(KHHY%3NU`Kzi!*_1@`Gh#_vqC?G7MN0IH)a_;gKeC!o?p zQ=>$qtM#Auw230W?E{7pjZH;)Fe+|m)WgB}0leUF)%gqZeR+NX_`(F2)R)TY!CH-o z-I~W`wym_lZR=WNOIn6JMU5B0R>*Hb{!f66G}DfkZ?vZ_R?0Kk@*FE8oJ=B|FK-WR z{w$%kxLdb&gbqL$dTN*XQHSFP1er1Hl!Slj4S7}zjJ^UV3U{V^|4bcjSd*43^Ty0YfS@`x-Mo z_kNWgZ=nU2@9R0(6hzyR@(j&?BW1*?Dy$(@&mVzont?hY zm7l2gj}sb#vFs$yrt#tbzTHno)TGZew!!*;LtKl6vIr~auTxKR>pvi%UsS+=`d!q! z)HfbYHm?3R+>f?saTqQ6lVQ?o7<)~LsMTGtN%W-p7atRqwgRN9S;?@+Zdnj)(g_w9D9;V(sfLl=%U2by9hr~mg=t!S=AVH`n2Cq2zBcQZYoGA ziuKKx{2UG^>n+W)M4>e3PvBtf8+67cyubN_vlZvmsg+vm*Xg;Dy{>57cG}k2ru(?N z?=`f7v<~`G%q%~YU?8rks@lL~Pob>``?L{ghab1QY2$N!mF)^PuerzQzD+X=JUP?U6SSuwh0k(2>Z_KTgnzE=fI&vr`7va~b*yhnfu1>6lxNyxy{Rl=&e& z1y%0Zb7d4>^m+j;ReEdkQGb$Mh0`@3jXSU9QvFJ+t?c-<_He&OsXMEMLFUY-o~)YF zf&A`gr(O^Y>qBu)OcU7e;s~?--s|M4Euq-iJ#y5dkBS?>eFgxeSM}&DPtw96UH7Ps za1NawdF(o|+=27hT&poT`~=2|ey@|uxmf`MVi!_1oK zaF1)?lAVV@!n}h1^Xlw!<_!OL1i>kT@{`1G`WlZ3N5{1aL5~;3pEdLYHvc-phz`t( zD2zI!mJL;D2ml$L7t@VSw8=piT#uH>!APkQ8A8eQFW{=ufk&Kqd6p8C-vyl>?5tjA zo3a)zhuhipznSZC&iR1f=Tc`|X5&Iuj279Ak%EGafN_o`0sgfDwH(&mfNl1HP1kv# zq}$Vi)1%f8$$%#Ra{^;lDFJ@X9N%?*0-%^>Tqy1Tq3Wz3q6!;rJwtbhbSg@BHwaSF zpmZb6(49krq|yxnLw9#6-7&z>okMqdIrrTA-E;qi{lnhxUhlJ>)#>-Fd=FdUA8sKa zFh3txzki-zWAixwtrmyxj%WgZ)SxI6<{wv-HWVYt%*kN9T$o@ph0)}a%Vdb1jDH?> zc)zD+XP1?NYGpg|ia8&6u9D1OA~`$x{>*xw9|^~`>(q%@GoOahaJ(cL-R}%CB*CI@ zTYI&#RL0{TqJF7gA}Y5vE}{g`rx99LU6_*JZ)l7Ps*b(MecxwToO!>9XU*F-@bz^f zKpom($5W@2bI4)`1HUSw$Yzt-t2UM*;AG`%{x-vYYd38dN?s$p&sJju1=9#8Unywx zPBTIti{1JElTKU7t&i1qiY>l6L-%5dDh*k87A-}U=L>^k%-DD247P$^vtqY{=FMI; zfB5|90lR-mLSkUmEBl8?trH!ldS2Y{ZN^JNKSsr1!| z=pfbiv5oB;nDb<=+oC34u~0Qd`?1iVh$lo^7|artUe-u*oHC|lXN@6xJ=9rhnc=he z$KRC2fRm^AGnlk#q~#3 zRzCW%>(usd?D=iuw(uq-FK9kM!a(BK^Q$9Gk^Aq9B8U(!I^bQJ8v)5$G+{=MoO+|t znZ%%e-s(DTG>p}iIz(@<8hT!Mz2mD+h;xEJlF*1nPK@3=d~J!__r&WWjeu8e28 z)(dz0=07wuPXwd7t%Zi{0C>g4&B`~Ccez;xOKfWft8BNbMgd2#_x@HXBdEmvBRO49 z);Ln0Z7gfvMf$npTK|Ur>AZ0I$IOk|10r}8;;;Gle`V8TQfoxmCEX!8cY-P5L+6HR z1FWg-hg_pho_5(96O)b;y;ZoF>jD<2TfiEp;cYa-i|`_gkwr`u;}b_!v6u6`5r>!l z6h;NXB}u+?*!Bi$iQsrJ43_h7ApRGng&WuzL36K9g|_?E_%N=e*U?&#Di$r0*Az4I zq0L%KX(MA;K0I6>F_9bGP`_nBPlFpM_H{8C4t4jvo2i#24b7?7zXVBjGsp7|hVMM$ zmdSq@!@+x4R@;3>F`vhswm(@a<6+*#;pdSYMl|o|&^Y*iCqvyg-2=Wa-9{bWyz?r_ zDI+^0MtHOSxcLZ6`T0g$Do<)H&N~hl;V<*rD7BrQ=4onb8FRTtw|R(5ECTT!UiFMU z6@a`TN#f5J+Nx?1bKR1q#B%39a5j;La*o!@Uw0d^i8 zymWbPzu;u@W0@^P%sOQAFNmmj{?BVKe@Co9;_6`#i%CAjJzojHWdFDHLG?6d$Nf@k za^SM^R-0x{b&+EQZ@6_Jeb|_ZGlyWn?@Upj%1mLGR^Ki1wax9;JiC{Rp53I^V=uYy zN%?7_7E#mi)|zL&?{@FO1&LV!#mbdC(F`)bH>-<8pMf&gj8dlkhVr zrA7FNb?iLl-%6j18Zq=@cb7$+ohy8iKwo~+Q_g<#5Tm~)F7A=$VE{Sh8c==9jEep` z54WBDPxxP4eU^!iGheT{dB*Y3r!rj=1K-7GDALAp%fN`yQ&bm;YKEYl*Fa9$b3{3p zh8!)Aj@+`K)=cJCgt2cJp}ngRraV^X zzFT5`H{bsl&AWr#3sr=F1$lzlrtdr7!J*4rFXGy|;{~g7pYr(f4&=YTma7iL6#qZ6 zCUaLc%yzz#vRb~}_adQNS9$JvI;9ezU*&nBooe6E(13dFAf$<=1Gw{9>#B8MzC~SY ze|g>w^4NJ#n*FQE--ce_Vgh&N35Lt~B2T~-lzaXwuH@M}6>{%t0E6J$qM;!fz zxDVlRgDvne_dhHlv(b==Ay6>xg>eb0A@bs=)_oxgSlT9-wDaO)hi_4u-~zaVyq)7N zHEwH0-a1l6A3hQqI8ax27x^W|K~{SHE?&VBy5lkQt)#?|S1xGQRnEe4wSWzM7t|BT zlWPMR-3+kqpSenh9NB@4!*Mk0HU^>i3jdUZ0rgZj?u@zkXk^mMP9@($4y+oIz9aMf zUU<5dl)pnxZ0yOm_qic$Daj*nHS)a(-f_Hf^*Vw#4RcdBhr}&pwFj-e4)_{PV4AvJ z?{NNYYzjxw1@ItMQ|-tt4G>#wGTElTVA$-P2_Ky|Te6FmR4D1R`*$c#WDio@VSy^k z%ai9T4VfJ_xa_FluM)4;Z+zmn*~gILE!K9^c83*y}oqHMOST#heG zIf8ziGY72tzSf58h;eVIr9!(p=R9cs@8?7r^5ztM1!yMf&etQZ#hs#BNqq!y zFn(RzfB5JC(>LAZy;BD>IaJ;+_s$X2OTTxCd|)Yy@mBl`0 z1Yroy;=b!PUC}$S1=&*Mx1%-)fL(bSn0|&_2RtCdNPKKlK^ZQqK^0T+uIikU=L950 zx)^3Guc~TN%Ac4a2X&A#=8MQ+aFbxV5l z#OS*SARm~PnPek~J@sPlB;-7NUo_I|-GO4bS5OUpT9Il;*UsUaTeYzzuSQ@en4$0o zRzrn*(Ru2#QRp1WGBVc-su>k=0X;5hGa2gDZG-!-wex^P-0ioG9I$HX>~cHOdfCnC zicQz{u_&^K2p7u*3>WR!N!yq231_>f-YV^%4`!i=OLL)SC}I~24bu-s{BcPti}*bM za`~^`7hcAmvi7tVP`13qfhL1L6UwxP8?e22LH#li(2^+_8pC--8&HXhqCpYJw|4%` zzXVZq&sP#-j$!tV^=l5m6`QxF>N;971Dh8TyMLC;fRP56DV_g_?x?W7wtGA6k=jvyN#XEA7;q|(SJ{uK{{Fp{PBgjI(2{sq~vt( zk9`^288?%GtUoGP>{?+6ufs(^bUxXPbY!$dXIrsBR#f`b{tDeAYH%X z)(ts9G=RJH65mbrB8PmYnRhn}<1hDj#5Cy+YuLZ`Q4f}iwIq3L^|MIQC@vUGhS5Zj zG+KV;Fq`kZyKWJ?>O zLjKApgzg1|I4!a%@UJq_ul95jv17Ws@74?e+9_(C914$(=|w|Zq#jUCmyV9kYs!?) zUpg9`O%+|D)aee2F27?YgzE5YO6cD8AN~1^yw|1}qNb=advXYpjA?jozlx-u=v&Am zK>msGfv`=?``@gJU**&GUO@X3?!T3PXtdt zDJ<5jn#PJqR1ctE8@^SHiQLNf$(Bp|DM?TXs{w{9^WWxw1my(x;&n~8b{9KcPZPEP zqWNM-WYckfTC4s$5p;dyIs3u!`QiGh#^O@Dhr1Wg_~y*H?}IU4HXc$#jJRHMT7L7> z^w*8yBqM^co3_tXO#`~ zQ&Rk@SliId!QopszkPc8pjEq_Yz%`IIXB)d_1Fi-A(}|5(qEeQzw3Y`fkgG7!A;y? z8}o49Eh|Yo!@N8q{I7itpvoHqRiNyv9PqOE0m1OQzVpw$}mkKVsQ=wv=z+tr<+q;U%pu(iBkoJG5*BM_`Fy zWKNq*@dbUD?vANj2Pvi zHY-TzCD~!$ygjSP4d-fze@vU@l5;1+=AHR78P~Um`nZ`#5YK1ktiUgRFJwacRL4I4 z?AEy@#EmGx8qVu*k_M}{{Vrq2<1x@>w?Zmo}zCT#Guqsa*mGWHt0fV3dhg~j51l-z{JpsIE`;jv92^UD znH5`1rCtJL+9^XbbNlJ)XnDp6tjw|kXp1J__bJRx;|?-& zealXyKUvr(5L6)_KosGC;#>FsX1|oVnfBz+rYmXe>zd`vqkE&)IWCo8(<#w}O|?b= zvJ4BL>3~;&iu?|_l&9k%E`^26tb3~Di5gw3Ga2QYWlPBw;MS74D^3sAiW}G)`>N!R z?5<=NtV&V!2Mq$g!wwbdM-8s6w*N_rf4%YCic#-;+F`$)g?5I$j<@-lBu_FMu}CYy zU%{zfel0t@e-@f)?v%!qNNlZd<|daDK+enf^*Xo*!|%!Yzn1d=Ygklm@ar_uYa>D^ z!6?j@n~O6@#pmhLznmF&HZAVO{#h9fMU5+hi0{|qSZ=&g76J?AO-3AJSj*jdET>EU zbPW8}uv4A$|2ZCTB(Gv}&2JAh4fmMhKxoCO?h*x%1jaM4r9!X|F;J#erqi=`bb$yU zd?pE+uDl!+>_U~N`7kgt)T z&P9HW;JA|IBk0!yIe3XtqLM~kcdDG7J>~kFog=5w7IsL{G71RxuQWqEY33-&P1&$L zT$rdaQI2>WiQ^Qd!j--6kfiKOH*1T9aFtwx!vCbJ$)+|#&>*PY-b%46l5u*S8+w;)_my2PhWCAe+~oQzFoO!=J(+Z? z+eJzV$5r(kE4IJ(rKZy#`BrQXd3mP!kCp~PZeYvqDgXJjZGSj>y4<=qgO`}ya^9L= z8!bVAc0M+?1_7{!!m+KF&Q^Fw*{T$FC8z!LG(V&rE%;F<#J`sW%&t0n)L}393Ua z3e(z7S#442l90Lr?>)z&eNDh)VT8nu*)xhJ{I` z=bmDx#ti(}70cBWYM8=^ZYHM55)jq$zfXDLlARGVByuodw)I`T?neBce-DO7=hgUK zOmmx?-{TbdfRev~_rs|0(b_1AtKCl7jFaY{!Am7WY0jn=d^GMhVHzEBWq@)?x~&Kt zCAbC89&I{5pxPZ;leeNBwfl{AdwR^)+|0V9Tz^j@{I76*2S&sSmht6RKHgJd&-ydD zO+qjPSCIJ+vEUaEwHd@u%o4C&9(l*tmY=Ap2O?|L8~w#)c6KAbB7Hyj^@Xs}J^TXB zUzyjk@_WtGE7zG>57Vbg5$CM*C*JJR=ZMYE)$xSGy+mj^26kEb6S$& zE2&`(EwO@)e_xszVNgOa)ESDlNM+LBsLImnw6cU5K@^U!PP3?S&RY#gBF3diDVtL(o?{Dr4l4u$&M4+r7_ zPFXrXGlWu7H+Xfw$|Y4;IreSW6G=@-TBmKrt%I|%Th5~!kBf}I&_Fzq zMr^9h6(86xYS5FYtXaWhn>jVrUkQ$L@~rjFK#w^u%a$Vpi_ITZ^#k_CtUfzb*dz?w zdOx+m_|1jAO!G3k`P?bFgG}nPd;m*+hjBJ#qiGHC!P&S39b7%1@M&W?v2YRXcr7_P zY>40Kw8SljKfjGW%?+I!HTmIwRcrinh1C~&p_b4YOb6|39l5|$6%ykNlLz=mF77uB zclg_1XXiErp@T2piu5P;y*u{`zlp=krr)AcwImZNBNsOJ0@W?+u12#|b=?!m9mr>jlF+NDJua7WU>57&+$*`JS=W!7?-(W<(t=$MdWC-%7s}h~sv%W{PQQVRsrJYmcWl#7ze~G1t-6s*7#YEc`$RW23d- z6=P%8&pdViWv2Pbs|{e_a1Nw5XfntLhswu(&&tYRu3J#eUT+0cPYUv(vQ{Jdn{0ef z>w!3D)2b3XUh_mg7IOlUb7jbttkJC^BeWv()$Ecj-xhl@_zJMYHT1iB3Z4j`fJG&`0 zh3R~gLG`qeP_5VIuWaZ}Vp2LvZ0JM}+f8@EJ4=OvLjjaqV@TFE+V7ncQwG>FnZzIK zx+2Q=dV^0ok_7-J=oNdj0%BH+SA^ct`5S+hhrm<#LVV+h5xPs1qk>3!n_)S@xua9lzUvx=t&3>nVR{8%!!ju~ zqcLhR3Bc@cnjBpHy9ej(KBH!>NIW`gZ?5^)2rO?+%>Z8g#+V3>aX@?zk$LUFpt$JQ zkF30V`PW{f%{TRA1F0UeXlbhQFY?`sIZc+1Mb@lyP}Lj&#FKzhi9(jBNeR$mo?GX}?k?C>vdqlDnYCd;cI|G%PP;cX6^jQ2?-0>1URCjfD=EE02bpNsx<%- zF;hcBsZ)+(H>5Tve-9riJMK`kx`L_1IL% z5r->18m(bM)Us@Vh7Kr#iBDbn1C=&T@8qP6k z`WOPxY^39c8}vbti}<>!^~gl7asmfl?;^qbL-BLJwF^*&RcA2tK3}HSizGXIV#$)|lqN?Ae8SPaAEZttBm;uH!`a5x?$(gx|n0?*hlm##X+VOof5vl?n;cyoF^E z#@02 zwf~z5x(sW`7P)a&aS3^C5@G_ITOWeXpIVv_;oiM#Ucad-d(?5EWA=6-?&W4pK`_}L^3U;~MZ_Rm+Ard>~l<4@atHnwV|-pK#8IKUF<_c?tA zfzIHAyj>9e&~5`)B(0Ul09i<^CG-nowVoL$=8JI1j4E=?Dzon=Tb|NEKwJfAAdu~bmse};)Q_Ta^$|H1Psd* zN*2?6ji4Vyl{0|y`uy$o`p~IDq+itts5Y-wt&P7^u7rCkkn z(6WF1hJZag_k^wG62kxU+E8(#>$kd=w4e8r%MuU}$o|({rgxB@GNq{ZLE&(*A-;%u zlX=_qkM)lA#!5^b1C0V;`~8uFol)v9t9DK0RK4@U> z%6?eHWveE``hYiKnp2OW-|Dz`?T7peB>kzXdz?nP72uHMywh}XV!=<2tEyzp->nwg zsk>xl2P*6viyxf!9Kmdt_d&`jd!#_(6p1~?J(B~626ube+0;Kx8{H}FNU4Y=wO;(v zYCn6vJO1t!v#NsdfFO~#WR{8Jb5|z4+GhpZQ_1e9T+nQBJMvD;F*4TQz%b?xqPRUv zHcrhs-L4VuWBGHhvU)uO8gLcg07G^LJ3 zXigcdaK%^1@v+2G_c(&(;QrR z*&ca;VD`f!e(DSvYsMM_JO}1fO(HsBp5AtXo7NpFACfM}%NMt~l3zA^X?HGs^0og5 zTAKjKZM=P*mTW_!@RAFLps}WSJ~9c&?4ydlN3$~^#(MHo$rSz_xd+?zs)!g^=)+*~ zWa9}EC+@NW8f4bq0lM`e$3d8Q#BkFLb1X2DF|c4$q<-{_%-ePM!zym|X(m#4E;~7u!Pzp263zbIhk_#g! z-Y)1TC*36!(WW0LYyG2)v0iLOtFufH`s29_TS|&asp{BgSiRp@ES;*D^jn|qk$gPM z$%>~1o}6&}TkPg&K`_~${+tT(QY@X>JVBdQKdG?J_Z}Xd?j`AfKZufF^nkgT`!np7 z^Cf~*A5@iaO}K>-a+6ME#m;^2kK>--_T?T`4%hi*tn)~fUCPT(?-qoe2Bn4yW5c$vx^!#QkKfe3YgqGA_IG3<7zF+j_`7~>yP4M zic_9<&`x(nVWYc~jgL|;$w}An+F0wpEs8+lFkd<=mUNeax2sK)%ez1NJ&l^a&*~Vi zFx^&fNH^a-W%bUHKnRxoR8$i`2=V_qRXWK)jr(K!10lW)avKegZu)9q6|9ZBFC!dyB+BLPis{d&A>V^CG(^VeIX{TH5 zIsGfmbW}Xx2=Pk2`0?;R`K1g(sx(ze%^b6&OM$(o&p{!xY!Iyf?K|riQR3x?TX9>k z?dKTFG@9%q<2|a@em*98a6Zksm@Fp>xu4}nKFLp{aiQ_(Or$YE~RDqQgQ^NxUo zyItuqfovYhx|!%rUP@NJ{4s0MNY5W3T`37Q&}SbM5u`~k;QRUdDpPd4kNSzYaZ`kv z(Z~XtXoqE%=4LH^6w;r=W%Ur!+gp>_5AzXOXj7@9C8Q+StQ?Rj@jwAq^GPkIhYGN% z(M1{7swnoy+KH6ENeX)6N#uqHy2qyQtwE$*qF|GTQq*>~+h(Ic%I%rt&6o_{X&D6l zhL1!az8c2iz&{aKx&9e5vSF@3C0Qvn^2zWF^t3}D2w^v7_F)P|T%BVR65G<)+1#QZ zVU*aySViE|)Ethi6Zs?Y!7rcs;uONc56BjCD{UWPesuHg%I>cJo}_h`keWbokVAAI zXBLL1s^Aw`u?1n_adKp9jy%RAKl0y5FTLuvQR9V8JZ!&ov#aLIcS`W3(JI~8^;6o+J8 zp}E?U(UP<|+cb&TQ?k7*hQqt#b4sl!&So;|rLnQ|e87qezkXRBzBEqXp+PCrCx2O5 zQ|MX-Fgvey75Xj_tP$~qJaAc!G`gx2rAf1*} z^LKu-w>K!@VJZHO@PtQx-9hMfAmBWM_s{n2lfs1eR&qf&ISRizIuT2Hf| zE{XZj3q!2g@NSI(c>vVI0W+lAPJ4&Nu?jtE3XT#q*3U7~KC^<|iKfdX-TzelZn$>_#IFT; ze%o*zwb!sC%&YO#)D?{<#u}wK#6*w>X}^CvO=~yq_s@xeF%Wo@I&CP-a~6Fs~x=iFRGXV9@7Bn1(k4TtxV5 zRyzJ#a6T^>`eK2&tx?Z-_?`aMtqEN4TG1Z^@emBj7Xq8i`RcD@&sK9Wxu)M5HGJ3z zQ|(G~`~G$8H&+v$^GHN^Ln5f_*hj)7V?F5szVKl&OmW=fxwb;cWyyz%jeDk}>Lar_ z$iD-+9GFQIoiO#{-T`@rtZb$8LU%(^ca_AklpWt((#&vVMLl15EKZNL2VaH#Vj_LS}(0_bGMrL zIycP5NN=9v-_4~7dg(42uKOR~%M+spYL;sU$13-aLX{Rh1b9a;E${e4fo!<=;Zg2*hKN!~G(+4v#Q=xK6s@x9Lc>i9 z`q>*>eTfAQ(BfpdwN$a;*z`@^VU78S$b(b{a1P83JV3IM-dL%3BNYDrqQ))eGxGngr*yzz_!>`4B<`_^X&Sb5YIokl$afCn0uVr4_KlOK-Uap#3WfMQ zjRat~C35MQ7T3L%TBV$vhe>WL{&~iM;GyaFvA7n)jDh8ypgD*sWD>DhwhzPLZEgW@ zw0QGPWp zHOO-jVCPzl(@3)Xuq5AfuAjNjAneBY%0jtK9Hgx&_NisP>9slIGkExiqh;Z7d>B*) zF3jb&%KxdAHK`H1PO{G(6{aG1k|F;7*>+@&lahKZbEbf2Cu7&fD0eBWF;?+v3-P(Z zKd18?HQ0}Cv5WoX_y4d7LXcc<-43P;slYegF=lhzk{g~YJKp7N0mDXK(kVu6h-vG| zZUxVMYeui$%$xXfzOlzw6c<^-9u~=(j~QJiB=ISe9K<#@{KLHrSehuF8zFp+XxBm( zhU57B6|yY=6Z(`{71+cWN35=h0>H1}i;!joBwu0p&or?(A>g*zWq5sLbvi+w2LdL~ z1PmJ5DvNE>n7NAL6+T>JO9ATImwm~(`q1~)Hr<%| z`Rf?h8OB)(AKY>#xd9p5=KG2#p%QDlY~b2Bm6(sL7aY3{hE83Z{`%_?4Md}7$cL)O zm*JZ5j%Oc2!qh;ijD2@&WM=!%iq+PN^g2?2X7Ss)E_Jbf6Rb`cfBloTilg8mqp_7b z@nmea_`Uo6LsOTY|HbY#SBoPCthbw~F7;z>&sQqgRBRrh*lV^GtE)TuroZ}i4GTwY z_ojl}JgQznMSoZzZmJ(|C!9535cmnZ8dp3adWvn`3jwdudzll^d zHaX2!&l%^$Cd`*D*)%X@Gqd$7xXawIqEPKn=;8xxZdpZWXy2>9??=U}(=W@vZu*6e z$o?~1V<=!*d)%=AN``mn>&%CtyA%(@R@wnseX{Dv6P>m#v$!z9dAtSRQf`bC^yHRA zi>LYvua02<^AoT;v2lrUypRyYYa06+&vD>~ckD;N@~#4})}G+-6mkJl*PHh&TvAPz z5WgcYyWc3xdFhiDyZ9NGnDaE06Rnqi`#U5m{o?jzbxi?kbk7{{H>>0HTe}VZ5ODE^w+0femT?~S>jfti^R-I9fL{_^I#`&rF zK8SW?e$r5iNx_HmR}9hQw9 zB}-yG@Dkxc9z08eV@Dh|rF*mR{o^_>^-^Rz6vCYk#vdc@Z$}tkF0AUE!gjH-Xu&Qf zd{e6weM6CeftPdtGcGCT;YkHi!tzlwU!T!jLtr%AxEwiJr)cYjMG6h~f-{-M4Xh^gl(urT>dyhGH|<-Y~8b59v&qqzlK zB8loR+%aUJ$j&XB<_kye1v2vVv?9;2X8MEt;9Wr0`#NkD?#qfEB=|$b_B%{rPX*Ox z;n#EPLB_HJw?i^V(Lyhkd&*Ty$+*9wje zaO;63w`b5IBCNeFS|^x}Xc+7)=DCA9NV0^&c)!Okxtb~Hc4yn_lxJSHHMX3e?cVyc zmglbCxNWE%Ti4G(oAUJ671@0GZre36uwlWIZmD?@5?0S*8>^92`FBFM) z7G2-!(|Bx)cicAZ>LWCgnf9pMzU3VQ>1wb!bFsM?eh=eln z7c9jt(@rsM{<$$=dLj@0pCWvB*U=lmD3xKCzo?k4+vW8Rdq9iFy>rs=D-dDgBhi<{ z%sVz_25+6zuM@ztOe;D1Ofcb0Tk2KZBi+;rF~v7myKE3-cxRg))n4_dE@Y-Xsv=hb zOwZw3^aK<2P2c|Wy`^B}=S~-T1e&b zbPk5G9Z3Shh_!>lGu10=F?)g!Ai<)0cADOKCz0xpQ_gixYHQ6d| z>ivwe?Bl=UFkYbNQ#Y6K0(fRB#9T@;`rJ?`o+(B~Xxd z_u?44CtQrv;To^@%D9gZjO12S>43~{n{8y<|E3-wjD|y$s6+OHg{*N6w0AgAI%s?^ z(xP_oszsDm1>A0O>%=(NzIeW6$A`TrZ=jT?w@!W==B_2xFv|z%nRmpb1?}R;2D%yR zkPM0UCkN2IUI-7$*bl#ha1OF}(RMy|(I5U)9D496JxKdLIKg=#_0EdA1hmt%iJ0iX zVYdWe{#l}50P#w!tEAzc%R5c#h;){3h4Vhiqr+U;-~L*g#qgyU#{C-IHgA`m@Awb* zSIAD!RHKeLxO#T>BZM!rMjKz&t6q|xq#gc^H`QKd z$G3k{wcUKR0gHlz$$jm4Q?Qme>U5;jw|!q607>j44?Pqc0&b6tyXq=5Kr2#dVupi4~{ht!5egRJ4Zwy9um+L1jf!KU)eu7 zJcKoAeLKCs6~4F6TR`~4B3&!!z4(0C>Ep0Enm~Wq`LrjllKqwfQZFZ~ra(c*&W%MD~?!%@;pHnzu6$V+r9N0O7J-;KnT0djLC{x}!j`W{n@}bY;im^jOn!y1jtA)ItS@cLVMi6|plZl8wE`Qdk-oc1Qus z)})^(>JXNh4xY$V5cjz_nB^8pXF`-pxPc+O)!Kl}G-MMj9L2)_8B6gpDx1tVx|{mC@ccE)ww?&Yz4n-E#?i?QyL zXg}u*gSE(*(N2wACjaJVi0_(bTr;r>bISQefCff~r~Lz@VQwQl5T-RUL49IjWs_SH z2ti3F8zd9>(N>jxq(MeeK3fFANmkAhB3y^IRX5113TGd{e#w$7c{=8^N8~&h+vY2* zd!J}KU+Cf5jcJ1|W$i6VlT(zUr`iO;^;U+)p~pT7fG>*ixtjx@@9j8Zty~&9fdyKGU+ig+ z`+}9n#IJ9&dJ=?qUA?kI=^ZAKW`d(#=kGe)>{yVAo#HMvkE6_QkTQX;%|xR#UZJcU zxmsC6z2A_hidi1@rKP|-d09R)N=+TiD~?xIhtw2Gvf7h#MKRS0vj5iwfQo?nVViP2 z+LARK%bv62h^MW)4STH@WLNF@ed5OTp)m65+Ce#AjE&9Zhv5eHp*lC_C+dkI zh75Q1lTM^-+F1q6^~>q2x1Gks)8$dElW~=GGAO9(#8@eaDTLWctjK~89rwpyyjsdj zBga=**K!7cKXmj()7w}298k;xW{GPTQccZuScxsi=b46-ZkOyEr>*h4dhCFv#?aVe zQzhf?`U5ZA)0L1JTA+}0Vlq!6cs)Od;45sky~ z6n`xF%jo@$EO`^v6zlmMZojdhZ`5+Bys7G}%MaOXql_bSYT5|Qc?sr-< zGgOAy2z{45Ayn64+ehqx@%^4F1ip+|c%~16^-Q!z&d0DiQvdhj(|{G1@EcQGvwlQE zpeZR02i_E>wCHdYDaH?0MShk`lxFF@Js4PN4+&?E)nLJjO7~gl9aHaNe4azc#{=?H^PA6;bKUS!c)(OKvxV^ z#%`8wYLB7d*lZP~&F~@MPQuUEg{VECqH-I1j}RLxx+}05&>>{bf^QkBIE#NK0{Np; zio&gJ;es~G0bpSsWE6mQdba!!n;PMg$jA0s{HVD(*9`r^Twf}IR8QMHL)A0Jc4O$n z(6Lj|RG2@nKXUSCCGr43r0C{QeP|*Rq--Yw4R*E)X4;=#t57Tyj;#Pdy&t@%?fE`? zuD?EL3!=YQ*s%rUfg5MK>zDWvFBO1Q_u*5`I&bCpo)H!faNOzh;D&@0rQx3)sEen* zK(!AKd|=rGo3wS%0msASa=%0>0sqh;%e{6Gynb=TgC%hATVusx?81N@Pf`+IUu2?$ zyvJ4Mc08AoqGdLskIEDTgWqk(FzvK|5zhSs{AE5>6w$VC$~`=dTCuxKA9zYjO#CJ*nxPYoHvUr=M(ugJK@lo4*~^dy(9IC-~Y6d>+Y8X=WmhoCLiaIiq$JK{t3FseSmpwr$vd zeH-qI@&gN3+7yibJtTImA1t@na>#lztT@TCi*E{>I2H-ShT|@o4fG%R#sFOOVBc-Y zCx`@s*PizQ0(-WRD`<@ZR_=INj9D>_*+FW7S;zAt5+*S?%X3F;C-@Zm$-~wjmcsL# zj0_E?%<^&Od+nQt0YcuCSL|P%!c>arJX3*IheUSCE7`mCb>kc)0wP(sP$BVlA0?3X4fCke!xv1Tc^k2k29-%sMGuVm9A&&pZJtXuqRNt za#p~@T%+mh0urCLicwhNu*3g3y`jR?MQX4u@%!%aB}JvE&kXTEVUKRwFHYoVfA!Q# z&!zI5)W4uez+q-aSgs?bs@~rx8bx(0`t*;fw=kx$_)rs{cyscJ(REz+&CNpBo8?fR z!AjGt`Zy~EPpSQF1>Euy37^9YmQJ&|9F3RKEcamly{U=g_litj1K`MEV!6<)ik&@|hFqvxEMIGD;7rEn{n%f&Z13?5Fs7-SjPAzan=|ii6$rRrO2K%7<(8;6z5F8; zwY@xHrOal1m)?kD`<|2%8{*fpFR?Uv2Km4DKaP5llsGb|dh{C! ztjH?Ej6Pn4YahA3C=*lxj+0Y0iDo~^|BW>b%A#6kJji(afp}CCl``A99DgF#4X2tO z@RlTegemYNOHS7O8?ZabAEz53m*8w_Z4(XS1~UwuhW4(y9w9iXyKd0*ckVtD6^a~E zHxu_Es#xh%pygUi#GgKVqf2z(-=*@wel5cdW@FVdu2TS>m~levfoBTpELDGEKGxpV z3YyzDGw_E`23W>8#i4<2-iF;3v{*C5%|bCCu_KnNX%;C#Y{7uV{o^L+foGj7Vw6y) zNq88_%wYDj^}j$V>(lHG>w#EGPr`k(FInFn0i)&@1deV!&{jdoDG9mv-Esie3qtUB zzH}fa>DL8MlO&<3?c7!F%!jbOnT5&Fs|t<1nQIjhc|>^BRk>cUdt;5a*tAI>8c`Z6 z;-=P+_%XYDQ3s|2W~Qy~Bx01#r+!1oqb0<0=6^BumQhi5VcYNwL&=cRDRC>^DUAq- zq#`9XbPWwd!vIQ1OE*eOcMLHDNO!{!14yTYG`>FXv%d9y|E@oKt?S3J);{-f9`S1g z+BU*$lb`p~-U%+Xjo>$JA326R5CZf7sB=MxL4Kfw9ciHn#Auld#On1+-o~#e_qNO# z52U!VW^mtDy#UR=iB6hUtm-J*z5Uca_!5yEq6M3#p8-=gT`98XC@mY4FrxGifx zy_;`RR-ij8u2L32Sc(w7C$;^_{@U$tnRiqQB}}g^l*7Q;%7`nw93>+K+@HJ#E3qx_ z`g;pw>DSl`jvtm<*nP%JUMf5LH>e)>56d&{zn}LMYV7by-`xm2_a(pdGot5Pw6lH0 zEUb>0`NlvRSbb)n{_-cn@vkRiKkm2Y7uoozY;{pL;<5g-yFX}IL4e!w&PKjcXxMqx zdo6IV?2vf zpR*{qnLlbv-`R}fkDSl{OL6$_!9BzpeHWz_z}gzz?(Q2{4;^y1&iwWMZ6JJDecrL&Ziib<&t^&g9IK=$*0P`b+TA?;u*SD}3iZV5 z{y0(RY@LKKJEv*2^DX$Z&S`6`U7~f-LPSB#0h^G(OdT*l^ zb&xY4g`OjT;*@2+9S|5Y?Z@Jf&!xw;vs87f%LuntAP%$Vu_R`xm=n!#cgte41}Jg* zw{~=IRJ`4%w?Lp7(bKsm{!^{e)IT_ zzc;=A`x61_NmZD2UXUbT27j{NoaO&XoKJW3z47Jfo86SSvh_GO3Q&dLkYwuDcOoD(9bVT`2mvX$205&^#3eno<1a%pjz zc}4Kxr$HN_%m|K*Er`Qu=zBPgkYn)e*kn9eNSF*5(aJGW?h;qj0sU>YgEzi=Y?wc3 zKW7oQ*m*1E&9syD_qWdpZ}my=F_)~4_gw#4pyEX(QMec%#R?8P^F$?4 zos6a%-AL{+uKe6Jak^(>I=&`u<7XvdFIeQqtxV0|!j{SrF&Nmb2Ox$L%1tfNR{ArJ z`w*hKdIZby+j0K{sR2-~7o?7@w!jXT_`g~LO4}fWivfki82hqK;S^(Q*Vz=Ll)<{# z%y+_UdI3TVE%FPpZL&4O@YQl-GM3f!Xm_bAFoNZ5lXi>R6W>A)N2sOztD7lSdL>HQ zq28g|X>oW&1w4cE8_Ub1`k!CS?Z;BBDkCi#0#Q+JE(wbuDak>=iq{j@MMAwpnHCge zR5>yTZ2(U$nKpchPj>?BTm_JkoM1wNd`#I$+Rnbx+>)*2Z|Hxs;&Qhz0Z_N7;)dshHJ3tF(dpfi?kl+x#7b>@?=`bq@#4!3snc)t2 zuOsxXOo-tZ&(K6koWoKf^B_E-^)RZyk9|rDOR|8Puo6Q$Ba*%NTFPVQk#Bwsul4@D z4J1Q(LK^a$2#lbum$8Q%uPGbUTdCi8I|OlRY`iwSo1SzE1YDcKGb*4qFD_{&UdY?# z&%FBF;Gva7`#?}GelXb5R##74VU}SqEEvT@Oo0vSd^S|-i{ygnzF2EH7NH(ukIQHL z6nTs=0L&|Q5Phm3cF103PXaXkuKJdR1Fsev6k7ElgxCP5%=;jnyGBKZCHrG9%+E4m zpVa3>=LsjBBToeL(5LQV_ zi~(CYOPF6)wm{%9+I!$UK=heUm(%^~d8h3b1zqoC{n?&*f{D;AE5S z8RpVPW7r2Aj*I0sk*yWK; zR|%+)cIoG$rvtpLK??uND@rqzSbRq9H|VVQX1L6+`J5LA{dxx+eK6BCe~+Tz5jbab z_z1e%*_#_FZlh1--sy4Hfy*bLMM>iSAgE$LeV1lZwD~nmeAp;hg=LrV9=D7Ds{dnn zdAOo!?@0`HP8ELqo^^VMiY3Z-T46@08PS9CRC(B8=rzy17Dlet0 zB*P}fM#Y90Vi@(U0ZtSqCMz{~pkLOvsg^q0kIJMNt~{s@UQ^PFkH`R^Se&;>z34p- z6?y9K)7)CmD@FEcOIyB;NsJX>UT}(|XSeS=%crj$qWJmW&3}l?av25pKQF-~PU7fn zi2>ZAOa>yknRzP5{}Y?F&RUKW8HLEBp?QsCxB^C`_;Id$S!*aVx7Ji@#2(nd^$i!m zm)7!({h!X6z4xsVpFYPyJw1LRy4ydW2O;{%mV-(DU)W$aB5>rwGYiVRS=xgM3HC4$xD( zq1O0(d+u6!b$@|WpMHyDXcIpAHC!BL7_{r4$iIY!)yh|qPk*YRtlC&hJ38aZVsS5w z^^a-7{8TpWRAC|U`g;CHoRx$LOe?me)vye&Pt=_IkUIY%C^HPHvHiZU8lVu$w1~vI ze#k$2@+sIL``N`|%KYdwb3&3EPv}lHJB=CKZld`lWpB`qYQ&9fypG`P3+8I{uzVgT z_;ZYsbVxqjLzwi^^jjC}vuesP+%6YloYITTJ>XJ$cNJm+u}-jsj_-bjFdO1Ru7#JgM&JsykS zvXND;dAKCrm0pisR*EZWQxtI?LJKUt!TKm{NH2~d4(Qj=A%L%Uek)Z0ZD5_e=w$oW zZ??>eD!LL2KWr4+py3^JVB@yt(+OSANGt)#_l8D5BU98ERf~i_(KOB(o(_@@$OFl+ znmYU9DxzL2qybi!p*~an{-wL!GZj1W>Mhx32BOk^a8iRW27{lq7}-uW`g) zPERX8vNm4<$ZzXR>|REyWFW3lCrmWz9f1$(cRlS`?d1R-!%5Wow+4@X15Z9m_@4Y< zW>|ekG8>vdtyaT}ENSb@MS`N?TuZfvlQ*|mRzT;O5&8>Wj{jDr;RXUW^!uR?(_Z?N6NO8ut$8THFf2S! z!!vOMS98Ll?uaw!yHYlD-GMToVBjtB))XN=^oX{*y-wML!SSzN3{)d!UWr zy90~YP7#bPgA)$jq>s+_Y9@tyaellF`eEunyo?qao@V?_w}Yr~Yv;9ka$b~~2H+9a z`owyw>p$!&DX!RxiNq_&q}}lI@^?dYh$+MGnXp?{c9oi;PVif%eVdvt{v$dx@pMP@ z5dk}BcMQWiO7x$e9eHNY0+mL;zQiG#cLTFzvgXoeJi? zvZBCo!NK_Pj<>&OSRClqo4qS$*Vz{d1TOQGS5zp>`3fiSA*=ir7_h@}BzUU0tJjAP zif!Af#qlL^s`QDezIaRr${_#Do~4cQO$9dkjQVR3-8VR3iqeL>c+RI3@6|lY%#E_J z8@xiCh&OZlOGKPWrgzz{152v&!2=bxzgG&zeV5()=Js^o7qfpaNMtPR8Jupev#x-b zkWAT6NWv?LNU3bVSQ=3b3*1WeY>k`sVctV+L^XTK#h6~=%8?(8ECtMouYQ$jZ8UX% zUtF}GXRpDhS5h02$1fa@l4i8B(=WhKzQxP8H5gXW z-Q^5PzAW|ms?-2=w3Y__Dt>An|F#0EF5}Zq%?$jj_0$CA(}CwhTnqcw&6SJ@?_$Sl z#EYZQS(@*22tPdEaC;j-+Mh~mg$|wTLT<;y{YNFJXHS*5ZHxp*x)8033_&*b7BC=^ ze?lAD!QxY$AoEMbdzqk5dg7D)`>J#<{b&EJgy_SarD-Zq?af#QQjkQ)6z37ukJBNO zA)Y5iH=YD(CZO?-NGGmhfb{%IGz#ww53ZvXDu&0kEC14w_(hAR67Ix1FJC-vp~Chp z(V;Q8HTIne9w-_h zuphJQDi!PQ|1=pWJ}nghb4JE1deg zxBvc8Y{)UIh-L{#>NRhVGf6CAaWpkwF91(auxIe{KSjr^_?7*ZWA#t=*Q@x2j5UVM zOAh@A#qOaB+ORNwZoTP9p`)L-Rr$i=?YmY2HGy{=OfL*vn{DwxSzE3;#d|%=_pjPS zfCYC`pGD-PR2t8j+Zv!^;cb1#ggl%6R;XvD@Bh;Bl(P&uWc@~(rPjvzdd^y25Ek?lE+(}6>(zG8wLJ_LR`4pEe{LQeeDRi8VyJ?3@8#1 z68gIO_wlNq_pp2FIW8A2$~h%up-iV_qy73kY>M;vZ+-z>i*eLMf zJnwG+ptnFJ-We_)_@i?X@)A2Nb@ZgJcGJ@;>&Uv}#kqrz4E#f}QOylr@G0J!?Gl6z zM?R?IYdYV^`8^?515qxOr@8G@aEBIx%TdbqdDnDWC!nQO&Uo+?x63MI2lQ(_ldrQ7^x06>G8@2qxPksznO&UIYOQGc zOiw`eQI|pVS&DhYNEGvP?>?;B0O#Tkb^5ilrmX3AK?6-rYbi}AVgjorh?zC+Y?yO* zDvnk*{{|D(#!pCcJpDah?=W2oFheLY8X)TtADoOZiU;jy`p-Jp;33lcVCx1b&okr| zPIa^`p){>}E4{M-7rs~xeKnYRyBOFcQ`(yAXYRVgpRLjJu&@McE>crF*jqeL(~*O} z^)^NKH;n((NVQL>`jZoyWeqQ5qdORPC-~6V7>A=glO~oK>O$QT#m`V$0gwh6e`i~q zDw+WAC00c@T>QrzS(&Jx;qHxvKO3pQScTMv^rIxCC1emm96%Tl+b;9*3iy5yR0}9(DDyrr_w*5vi8qYS zvNceyzLEX(gk<~)LC1D61yjxv_3>b@Blz&$Jj@CpFxvpX!j%c_tYxv06Id?!O#`3u zO=z9?7N-6cC7qvrYkuHu-f2$nH6cutPifb6y`i>um{D)Q1f+4WAR9R#K`f<0evh0`3&1Vm`icBz-rU;L?;L z@j7>P@alZB$+7s>y3t}0qmt0O+i}t85 zOwlhMe&1>^LU{nLv9DiHXj7d1CG@Dix6D6kPTgvtk;G84z~%J?M;Nvc4Yh6k6kTW^ z^owuY+(UwdP30ae!7-~}dblSP*|XB|5s2GX>&_M`iBx*(o5(pcd`+1HI^6ZMj~ZLj zpikX9Oflnd#Cyj0Bd2Pm;_u#wsFL<#5Vi{z12Iqp7a%U)zEylN9=w%T68}j3*V+&L zZ=2bHxPlC4PzS!$c$Om(^(0c1o|NOIOiqY7Zs<_Mt}^BBSS;X;EMP-u8ntxU7h#Jn z&;*oGUHA=_(5S{AGWc{a6GpkHP41Oxj64duQe3!dGx{h8*Fzq^YX8&uZ6W30G>!$T z<7khig&T^d9?Mpv=F7#4(bBOUz!xyHX!HBrR?cQgl)pD-m8$Qr|Cz9u*t*q6U2G+l zKb`WIgB-UH5f!=o8VQK&Zs|#6wH+4#&aD%l)0_6q>F0fZ=*y6%=5^&590Q?(wsdkR z@7W~r$7jvL=`8Bgru@^zdiE}1iXcxuW{0Iur0Mth0QAgJP#vAF5S<5|G6$A)r`0pD1tVb*Twz8ki1PERaT0GVRT~8`&={@rlr)@ zLtAuLjn-(<$=ia!Uz>aP>`1|JJc!t4Y(x7ADR8WU)>YJ@odvwoZ`Wf?R;EP`1hXwtn$!{RfHbHvQ|AyLZ zBF+R%r4g@7__b%B&pa*0yG2$4&f3f!r3`7dLa8&V^{Cb=YIZ9>6SuxFU=JdB!c*B3 z`Pmq@67UZ7Lm;lTo;sjcj({Vnc<>XWB!Q|7&jQ^_hd85zE3-5mA(1nQ+{6Cxg7I2atQ!-aP|JfUze+FX_Tpx$FlB zhI4Hul;{TZL@26n$V&Y+q!w!!pI49cR`+zmu$a>IB6K^EfP&RA&nt}U#|(*x94CH^ zvycfk15Aq-<9^@{w;Pqit)VrgV(9r->y@(jX>KCT=|3!ZiE{2MXJFh}W?EANpFq-} z(N5=HV|Npun}!S3dUWcSgreDipGp=^4PlUSRc>ih*QzHWhP;X1P=CkAnIl%rFyneL zu$k|W&BM6($XdKnA)p?HtHWDk1pF`6*K}K__`_*gr!gryz@+81@L#OL6db2y)MuyE9#SJ=P^fj5=nd(`ok@-9 z4#MW;{!qJ9Fe3|nsQ!*6h(g1wTZ<3T%FDA+83J1)ND4khn!>uKz4=~<*TjYPLQXjm zC!UjWiuhCfuLzGTL{g#~WNnS{-s>)XJ07H>F)sa@d4{!YJ&P8(i&_I;3}`r-Qq2da zN|x9-4#H;{oJerdCEp_H+2uCm?n#9?Xm3&mC3Vr#M<}au z7uar3#XNR^V^q0P_8XkwlFgdCwKTE;O;!Cfjbjz6KU~BVd@dd=jC2LYb^QGpTE`Xq zE#j4U7PT*D1hOl#;Gw3?PX^@)rj7O_$mz=U%>2Kcn`li#0|GmyG*8m&$8{9-ue!$L z*Y~Bu`HuHPF3E6bs?#?$U6;*rZFGLothnRv%J#dA!oBZP`5#U;Zw=xAgxQQWg&Odsm}YFYM$!N#ox9nqdShQm5Nmn}}Mc zZwk@vPBa%!Y?h_`S06GzbYxOip0fiK?2EQ{aQ=~?t4{QS`bSN(K##!3HpXc$18TNY zGAgIm9*W&i%6rdVc#8eMXboDuBvAfcP2+N{qxTZR+g5TQwHmE#I3Nl*4|n7WiHx@T z0kEKbQxZ}R;D1OVkodS_4VEhneBgQA-S0@kyaZ{(f4n_?4%!Ux4$QX?IoH-TAMF(Yr1tfcM7Xxgcfr-=%sj*{@Wl5*fL@!rhzv z8@J&4Pk|3Pd-zSw{)>LiwpJXMlbO~HJV|i9{Kq=859&{#0s|)GKAdzaKO(nZNh9+P zEuoeNWWp3#b%$oE#T}EqILW-3|D9Icvc3AXX~Bn18R>!zXTI3=b0TK;k7{ydVt(vi zc4!koJS99D@BC{!-NY!>6PO9@ly4aVSdkSk+EfM^C$qzuR%BCe+kAD(>g67~Wb>T6 zHre)&kAYUa;H;I*mypB|y?*crbHMpy#D?!B|0%d0>}fgC6LQoY5_N;0e7Sf4uB7cH z00SD$Qr2hXYL&daT24n8O~LoaQA0QN)<;cm^kE}TVg3KrYK8xT)<2>7U= z@#Gxv-r{E)f(j+M_)Vw?am@7s{tnFC>=Q zqS4jQr5Pq_=iWR+$WhCCU0xIk<>LG8Eu$6Q%yOLx#v(Lr%g-NgHv)cf-r+3uI186o z?HcRa9s4|}84h%Ti>vFw{g2o3N05*d#uoWk0{Z{{q#CX^!4ds~s01=7oy|)9X-2Ia zz$hP~$N&!#a9j2OXaj$ZjBhDvIUWG2a31P6t|(X9UWVPYE(JeHBB6ahUEdy>u&kGC zv>yE~p={ex?0fp$mR$fa0#ns}@poZ?ulPWJIz&-rS|?sEJJp7-L=14p<<;VE#hlX? z`S$~S=0BTfLK~r7f>WiJ?`?NmVwMSYIHZtyK&=*W&_vyYoPoM^Qe#@)hq!uM{@0KH zYG9oz@gqxn=#ZVSeCJEo?6gvqDC~@)Q7(6f1lNamMXTN?I~9 z0?|``VhFN)wDNpekLCzln?Ur$8nofbzMOF&OBUh36V6?ZcLEy38S5{I4&M1&9gHv= z@Gh9*vDP7%Nc&^(ZpylJizWYc>=m)dcxpmb3iL!qJy~Cb^P9|jJ(NG; zfRD=Z^{b?()2{>QUlT$spqh#2^frr}^jm~7q!CG^BWKQDq$BO`66zMA=H**{Y`o#|)H;iva=wIrOS1ZlCyk-jduI_@OFsjZpq_OfgQ}f|H>mXY+{#(ZLLbyu+aJd$x=Saa!Iubw9$h8$ZB406G=Q)=JSRI0H>85#7PEE6CfcqCB)a+ z&}o-4+Ngv3YHpKuqZ;&L58L{uF-H;Q4}c|3Um207+7;b&#;t_cx0wBe-?gQo|0;h^ z=Vxcz;2Oo-#I&yXtdefAGxO8574Pu~EB-{LRN=_`WW>?orI)XN$T0HB%=l3|Qx zbwdeFwP1tKKXoaatF7I1>BqswdXU@gnh**?0s(b39U^N&WK>nRCtO4XVF!|~0z$Ey zmmUbdE&HLo&7cXzm;tT2kn^$7Ld-ga&{pCQT+Dzu`fpKx|2 zB5b|B>~6q1yvrWC_3BZ862n19@u{VJF@stw;;!1n|4-3$Nv>*ijl5`aJ)$&T5$bSy zx~B%sh*{x8gzwqvY40#A0GbK4lN&vDpWZX1MCrPMJg1l_;BCC`^k@&yp<)wS9c|

    C%Cvw*}666(T1F=b%x%CXjdcS33G*4}*h+j!U_Zt3@I<|}rRjzK6{#!c&C+q;ztCfuNGC(X$epNJmmG2A& z>qWhv;l4@(HsM@1@E#cUx*r6UCjnEO#uQReIH{qv&C9NY4_<{OU3Wf*U$#Afro7xJ zZ>Hvu7h%?cdVLQk9w)?ri(7=?%n3ypjOZsWztMvt1C{iTD3l?_9wAk zb-MQXM!XUTOG$&y%1)Gwbb*c_|IU)^dxq*iVL-2_Ycj8wR+`|xCClz|na=MpqSDKG znNYs22L5Mw%eQ0$&P|`2%^a~Op}%Err8JvXxnHTQ9RqYPR13#MByvJ^N-Vwu)$!{N z_yIYLRE+OD6Jc@rTEHQJGY3cZh99U88xR5-0LHDBN}!Z79;Z&E^7$cHVc4_mkF{qV zRRq5*$uxa_(#dTFWfp5c3s&A2FoGF8^6U)UAV+Rm1YiH9(7JgSQhF?g`p_P zDMzA|_(Llf+ELOitI99AkDxyCwoXBsCg6Yz`h!<>*7BbK!cDk(Zw~$Ee+ywes}7kq zLm(r@k(4p~^A*xkdbGSD%gblbA5*`<+4mPc0@VYn=Ik%w{sIV34GJ599D@+2X)~xe z+=K*dD83jZZXEtD9RF8sS)$cxZ!Dm7S6D>Fbe|K%jUtj&1qKWGV`bHpyuMtjtU~7V zg~s0+GKl-QNsdCsmsMG*ZKxu_MjDa_@Lbu2AusthXgP=k75RcqbERB-i^zk@ON&YT z5S?NDiez5~m#fg$go4?GnR|EEtj>4O9F=m5x+mZnSk-Bi2%YLtE zH?}9#ANPlbA&41Uag&wjsAE_B#G=3TbeKlBOz?4ur~aP zc+~b|%L%4ZgWlM49fPv`w_k3giOGn_3-`$?^z$(5`qNll)ZF!ORQ)fBO^vv-zq(=e z*wMF%*49x}KvSj6$!*<40fCCuhMH@2Zdpny-gv0N6q|97|w9m#aoVVrwhc zH~l!ghhNkF!sS0gSgj7=nU!GNn$%vlS771Vdxb3csFFyax|?_X4N`!+7XfUodw0>m z5(*A8Ua;<5*T^R-Cd|c?;CSk5z2BPU>s3 zt)|@amWh~ef;5UdxPS#);!-*@5{`r7CD{=7jBIKN=iqst^(J2SEaw6lc2yF;`dbjG z;|-9c2J#9&!8H8sbgWh@;a4x(G0RjxU+{(@S52~wUWuK_cj^vJgFzoH-^$XMpMUFI zwnCk;S;1SWgc=g7z?az7qd@1{XjF&%kZXIUdW#wE2e%ERuX$MO?X z!^_6oxBKg+Bu&%vOda7NhN3c;EBym#4K^Murx2eZ1}7y$#lf4Lk4pTwXl+8QC?h3Y zY7$9_i{||LoS%jFvJJvW!wr*Y##uY^x|UhpjfvHqn>{$n$$pDr5&begL;?TXizl%G zlofX4rmJ5t8~k$B&nZTZ9HY=N=Sj2hk;=UPwSPV9!gcPv5s_P&FY)fWTe&1Skc@jq zBK&C~``fAU6tclSAfVkmTwqKXwd`Eekfl&rgpqFh#EaL|!BaZ5F1yPL&zxSBoTnyr za9r$eh1y8v?cNuJVWs0MzY+jM<$qw_e;J%CD!|8RFzE&)+Ux|IwWp$_60P&44c>;9 zrGF00v*+oyQci`_0~44P`;7-#Iup?hs=b0)-Jx|E=Z!@HP@So*_A_P6 zfamAGOFqPTRiT{7Cij60f_AUFqF`-WqoWTwXGar{Gy?EfgUJDrY}^Nb%E;4ROaX$_ zM9mUtk3GMU?L}!22F1%iDv^r#%EToP14GU_JdZsk-A>Y`*B(cM->ymd3XL%3`u~Z# zko(L3mn-mBoSCLh8eehLIe#&@M5|W2jDXEuBQxDsV)WZ46U~qke2RzAq?IVI{hcCh z1LDZRRNxa{Ot{2tp=jJCb1Cn)zktul=%(+876Ao$K4A9G1h3cj$)iBLFwU`%)@AtY zR@f5c(|Iz2Z<`O)z^~3<2qvx03|e+Q7%xDLHOB}e+e5ya03#S8%p-4Jzgvp19|TN9B8d2@#epd!L{{*MSw&8X8t zS>VU6XIrl$#(bbNWLh7`n>ed+N`KiI2{?rmk5t*;v|m+9;mK1{DeVhEX_Y^Y6OD1( z`7xh*VQ@3(pe|rx7C`~#Y8>ORR>n)xk)zrJ;#xar*_@#5Ry!3vGajaTt4`y9SGZ^7 z94dD#PFn^>J+}PTlTAj3Dg6q|tb|UscwvlGA}~*$63UAu;KHQ|1-9c?GNb?M8D0FP zvSd!xRk*{1aR*a*hsFhQe~JOD<+HCVwX6Q0ud#(d)HSenU0u}`y^zlWIE*4^pZdMo z4@!s$bH2gmvSavn8T33k+<2Fu=Do=x+_H*xhaecYe|(Y2gM1Lzu?|sZFt>nE;TROn zwOi!LKB>*N!0raT!o~`H53J6qUoI!q3vUV4m-S4ZOdNdkRH@l~mHPG4;*jJ`VKD2H zt{rYSZK`Pl8(?%^%JUFD3|w=1WfVK9?d{ihrl6WXmzc0L-IZv7-lD;xvVQinHmd$D z|M=ee^(`Q-KJ(Mo#9GD{o)s*Jc(Dm*#aWg^3}Ny5gLARO|F0p-?xQx77GB^Tu{W}{%dXSa-M;@{7{E!XKL3g2hGXY>4)<&$_!6bQ}|&U>M3?+vs*N2+qQAX z6HO}&77dwxJlJ+jI`8=5OlI$x|Ix|s*uSKvc|>Qc_;ezWq~!1KdAS_tJKGc0zYoDz za_dZv{}II>Y3fjqL6|0=S^Hp&)>O#{n!|2qwJIj~X!PH|*VKl@=I+}XrE@9y zJZNH0TD&~9{O(X}W#DF0v~qKi>B5zuG(!s_Gx`SmON0Tlc$a_*qZ1gta@rKqxm{Mz z&J-vu--Y~puSO0vlw$0Jbiq#a7Y{K%|M}KA5JBrxL{!a`^uLJ##nvRPq%P2%u?rq1 zd;;Y)zbEo<8L?f@v)ys-h8k%&6021dQFBYUF((mnf@&Kbr`<^U?~;wYO{RhK7kEyA zj=7j^+Dw5KbQYqt@igwDCJHCM))ks~YoNwvDK#bKTpXKl6g}QW3VOAs{vG=1e$4$x zHldBS=}b;Bg<7)0;>WUh;-xO6o_7G&>qJ0%tBR&ue*kL_Fm;Jje znHK*&J0qjdcL5n2{Ee&LHPThJONyhVXNmtmcyoyg$jO;K)ml$1{5SSz`pw2alVhr* z3WAzDngAc`#PHry4AxEdDq&Wpm1sSzYwqGVGAC|^j@qcs>xqz_5 zZ_P?7tOI)sefQ^_-=FSu5Nd-62?7C|qR{s;<49w%ffHF)%odnr&bWbf z)DPolqJ?7u#Bba*GWr>$8~y}BH;rTo$$y&HGO(&tVut%442A_)tw|9w_CM_NJUzWc z5%2+%_ocH&3567&;}5Fq{<+zIK|vu2rB~GZRk_ZlKS7~WzD!#h?u_1_I780cndlO;Lv)&(VFtrb=EMEgVDaP zUfH_;$)7I?K~JLjb5+e+uZ}OSyw{R6Wr!5%k&Ie|0u@7i2{qE^I_G=z?47x*=N`>P z-tCtl0eEzokVtBuAMS6l$vha=ac;obp6Bh2Fm$K}!zS$*I5*7<28T=);;|7&+NXpx z6S<~jJ`pfVe>$*CFd;o3m#P?`->lxIW^17jUZ}5RHY#cRmOlLYL)UP{u{T?Eeaq#? zeY&^eotrz;#~{4LfiR^|Kvj+u3=b^>zX=JWz*sgUtq8REG6T_3H>j!ijIi5RxBF=) z8f!t59=z9yB8*Wm$OrSY(x=y4M|^T0qCQkZ02D#+D_PBWvnqm*L=>aw(o%D!t(QEH?f zZm=zP(JQrw6d|z#G#hGsOWMx+AH(7Qb)}c9fySlJV*;HXFsQI1o$ET8TmhxiwJ7qo zdjfNuM%wx9pO(gj&0ibyZNF(1Jd2KtKM5FJi{4BHp$VKT=g=2E`7#XhVZ`7z{xCe*e&)z&8# zieF2kD!9oepJ}p{Vf#xw?FigieM(Zdr-BkoA>7T6vPZ5tL*74bZ|R-m;$|63M7V3n z=Q~BGwlW}Es!C{uk_a5rzIRk5HP#9^%pK`e_Y|WL|NZW>((^RxNoxTeYstI%zv#ai&Ozv?i*p>h=(=(qlEFEjk6+R4>G#BjSewRqDZ!hP zanCQ7{u}jX_=-p!0wpg1-|bhj2~{1oT*Nl|w;n=EzF^mk1@GIK$MOTGB;)VZ+qO(j zqWxN>y)t0hZ&e^pB**AHQ)z!*(X|0s!23LK#dQqAzQM$6(#CSNv>N1Sn-F~342<*|J%|5TocWv=B|EQ<0rmqH zMv82}R~qcN^vQ;4uw)K+^_3I1;G1u<7B*o41s!fKDStS2b;ScaWZcYTD8_Fp+&WJ9 ztSMJ<3}^VJ5^%i8@-s53KwX2hD_<{5R$A2lF?lrv&Q6uDqez{|48>NBOkYG$5#I-# zY-+IAbrH>#{n^HH>R?X>R#IAZK`V3qzbt^ci&ycqLQN?`+u%$*6b9CoUO^;`b9_@} zS65&k1{#g~b8j3gQF-6j+dazj{@g_eq(LMiIx&OPEe0wa-K}1APTlw)m(|l1cz(Zn zeZkrv|4e`CX?{~`2Wugqd9rz;_Ln6e;aclgV5R=qQGnAwQz4_1Mhy#=hZEiOUVuFS zBqhQ(Dae(dprgI6Y2o`W0r2AW1&N!nQl8-m#L9dkAnq^y@0b&rj5WF-jo(kGU}1^y zjKrplv)E-i{hoW4l1;C+1%P6r9u+b=v8xFw^xAn|vfWl68YqRtdWmBCf(@`r)#(3& zRf%J$v+$o(&?ZmKkJWrMtCW?KjZQ~s)GJsM|6OrbHN;G{JSpxDYZcq-HTN~Dv3+&E z2}UGFH(qePiIWewm5yiZ1zbDp&`;OzsxRluatWA+I-o+GJHNjzCD{@)K|xXP`2kaT zGG0P5=Ocp`9XpyuI^(%-r*(AJIV~Q9@)-gRlK6Op@L@~lor-0JJ7SHxP@!jjnA(d< zJSJ6ycU)0O;-ts`pKZL{t=&>ruom4?VERhstz}x>{=NFgF3()X6~GBRix1brO8`q{;YUFg-A$=<{xsO51_YiF$MQ?9#=p*T35*^WyXZ#v*4m4vOE z|D^Mq=lg^H;dVW9Ig}%l2SEhIdi7YSA0Sd&wt!$N;CbfDQwk?m63yiCHui4iTfTuQ z4JIN{KnV<4TtFM*q=<0Vh+e8OpQGO8fuX3!Iw)YvEDXWlmt)DTuQn}2aOSCuk%yR` zpGn{HzCXClbgZ5coUwR|g9W8(I(TW8tpSG&zW;O<{v6ZGWX!4F1hkEuW?a=pxtgPN z44zvWe01MMNd6be@;V^>MBLLiQ+|*4Hp+`gppU%5;1?}?l7 zOG-TI&e124WlBr^tpv6#qwVHm{vc&S>GJR-u3>Mqu@BaiE@#Ka0}iBe!QU}$ORkcF zi7>WqyZUYXF5$Qe@Vl#Y(V(QV)&NRfZbM8;bYUS`ZIKZ`T*1Otf`q+do?13q>)K12 z0ii>WN-4A+!;Q1dPqDgX?`OCN?_l+Bvs~R)rDYw=K2{W)oZP&Tyr1(HH!d`d_8%Sxjlh69vL{FCp1K0YzLge5qg9Um4|VIR6@uBmM+UYFL9osNlO;xAr!DHB`=D} z7D=hc<0lLCp%wC$n2z~AwGut#;VVkC=lP9NXdbe2dy2!uOkTLBwDDPI9Q0*wT59#~ zScGWUd+mV)IiNd*CwB`@)}nqNU&AW|aJztT@oE-bZne%R2H;WT%!TAf*0niwjika3 z0K%iB6?3Py!w+1y)uhducTeS8Q)(eTL?&%7krqYb9h=mTXpy!Z*GzNmZzpD9$A}9N z!GZ2tS>&HR;qelwzrTXD$oWle=(k>arkfb0c-_CPQNsU@qlG0Ol8-Su!Hu<{@?yoW zV75(n$;S)nyt4j?pdInt?9or4%Ti;m22rBpoovim{P!k46vaB4vvxlKDvYOd=dS0< zoj#<_6*y@nd^Dd-E9R|d0g08*7^yhgO7nB@x4+_06X+~fHG~#K7jh=>fauZ|KRY?V zawRVx@_)tN-*=8RP0G}uRpJ=B|L&-NWX?tTD~lQ^TA}2}sKp%nVWrENdJPFxYl{4` z{f%DZzNKT|k)rJpAMC69+>nJTWO$DI#;rNPy=?E^B;R!~ zwXu&qXq|sJt{tUl62iZBXJ-lq(s=3_vvpk$6es+~J>e=975Mf{#gdH0yEv#Q32W(> z?JmucG}{+UxQC*R*A0_=T>DvoNKO%?mjZ}n`0QE8uxDHPv^6THy^ZXPbUQN$VL89r z5V!-}n1x&zJlB-PX(<&rD%-)za|_WV(te&*Jem*Ch2$IYm#7#%p+V~0p=bdLh2}SO z<6ysrXs<75KTsI1gfS9#au0$kX!--|)l#%l9@nPqo##?(rJZuLjG` zqoa+=>{s4CS0~EVh_AUDJ0H(^OATS&*`a170`?Oe=7s@6(hk$dxe(0l)VT9PyX^Bs zV*3XDo{(e6P0wfg@JcKJ`m+4?X#Y9J|3lSVxJ4O%Ti-J=tw4U!Y72que z`rV4IKAnyQa6L>gOU4C0zUu0yFK?SHZ>*Hax6S>jB3;m2{7Pm zZKMGfb>jo8x3MEF*8k}|DDD|7&NLuv+$Yf`<9yF%Xi@j1b4f)o7|;PW0srEO#h-4n zpIm%*;&$mB^xugQn;6Ctj|m>>z7EIJLfB%4&m1nqMkopcBy`cG9r9Jm*~uIV7&x_$ z7r8*@sbq|kQ!4G%+Co&)={Y(rO0%A3YQ^&cjM;vxIn;mk)&dq`bVoL}5%5QHKz(?$ zLG%cLMYJGm^)jX~XNvTM*IT#5#}je=oEXcbayCzDzyum-;lNbgGmV>TL)6(>YVoA9 zps(xV{oG&QRXp9kI)BDz3y9*>i6z1-wsmR|ESx84xfpdl;OdtN;r0GwsBeW_`C^5o z$hWblY68(08>&f0e&`sLqf1aGBLO3-_eaL3o<+ug)P}Q%-P(Et4cBMgy{3b;u-k!n z=JPMGav~z2O$=3AS>Vx}_sc;4rAh{P0G{clxN$=|R(+dGz+o__p&wf879A~SHLD1Z zD$JIrfNV)9H2Uy&4Nz2IpnH&8$AHYx?*sD36JJh3A#n|st7X_8Vsq*wHDaKsXdk{R zaemVb^_PTea1GH9Em-yRyl{Nwq|1qnsaNWQkb(jdtD zZjxlNVv#*#!k4J6iq3wE3El%MGQH1v+lXxl&|PNNeY1%PUEa4#4f}N5{O(U6nV7yJ z6Cxq?lQqgNSj!Sq&f<9olKc@g4tMt`8L0@=;jBxS#Ha?#kBQ&8x4QfA6(xc@;02Cz z>m>sN%~csl9!;L6bq_2ZIgQL|J4m?>r3|{Si4HPT+qnM~f98nXf+_TFV(Exd^R7e5 ztL}mfFmCH4wR(RmJfS~-zqT9rh`Rs6vHNjj^>Oj<=;BK1ir1a|q!&#f;HjxO4*uEr zfZ|nM;#8YAtHr)-IA6{Le^a?Jf%tlW} zRo?OPr(_y`X@}14)O<1VII1qIzCNzbtf#NMRS*H>2e3)OL<}717Qc(Wl9<#oLTB&% zd7wy?W@$mV7mulQtl{n1e6sL%f8*w2e+j}JTiZh3pWGf0^^g2=D`)KXySg?igX(Xa zn-}W33G?j@(>rnfQ=*j@fqk!U?7wuQ^~V(*OYaiW4|wNYfLXIb*wYZTMKpNf~oHTDaPjMZpZxhPF*P# zWe?!*e5XbDJGEWqRr65RBf%j%?zQR<-e0BQQotyZP&7k{W=nGF{7tX(X%5XBo+=nH z*gEBtnt+(yTW=v)ZoD!f#^BLyhdfbmDp{jtCZ3o|u`@Uau7#`zOj%#1v!ytT83^Wg zSV8~6e1dCjyjZIRRY**1!2nvdN)#Tj$o#pK$w%jr7hUBAh>;5VWeY?Yl>P-!pPQ?h zqv~j#@1&}zNT*+Ro+PCRPg|pBk|EE-DkR?N?ImJKlx(4QrlQ(Qh!3a;6Bx`V z3JJWQ z`?`}qV208evAKy9!?h%?;t4kPKc=dd)zeJ~Uqk%R`!qV8)%A6h4$PHHjPaUlorSwf zygE|lfaXn##*sZ_jxZK7-(e6L>X~MI-}aQiW!(INPba^%coI#}ZdM$lF{YA-c^xEO z#x62BE*4JE)_Pa%FhY$}51Ce(2dfPxq)xT#!lgBQl(>hr^04bQzo7`c4YL-6l%RNm z6B`*V)vx*Ddq+6f92Oek^AWaK*kX}o1Hm4ze!PrLFGke#&LGh?bx)i#<-{kA3LQjX zEJu+Z#KoPK2{=WE6pVlw#W(1dl%^Nq-SW7_15qR{VMz`?0e<&YBjuQod%@Ll4rK5f z1AduNstha*$hba<^DE{WGntY)bgeI_D3_M0Kd;j6W&+q3-R(!~K+?vCzZ*t@4y*!*W^{P>N>=ExxF1ysIdJ4(*M1()5I?3<|4ZnSnL1uxmT^L z>G?eI3KblAxpYd66G*b_;8T48nVG`HB-k&8e;N*WaUGhRmx7^7Eha9uqV*CsA5a_a zt4X=`srylo8`~CUAsuF$UKyxvi6dxuM$}>biUyhb^h&Xgu}Ur4*heG*QB#DDmd#Mb zC++6!E)Sq2dBF()G|**QA87Y#!cvblF7@#BffnIW?cjZf{#R$xDgBBvv(@?;saYz? zT!u=Ji#prz{cBkRXEN1Ty4dLx0Z~s#_&AC|uty4o^G8VuUYkETx-eJ{k*C_4t?11h zbyP7I3gPVU0ZN}#IVY!}6pp=EVfOaA^yc^h3B2lu!=R^R zSnTEB^PqF%!iVP%8FUd5l#u0f>sJD~wy2E^fI1kUh>zCS{(h%#+=}tGBV1{k_&BYE zeel?l!3cDHj00 zV-QRBz!_-J*HbLox{84Mwq;B^aFR>5NaJgn+7Qa@*^fQ9UNC}{*YFNzZt0d|KLR2m zWsW5%5}7$>w=!Nx2Hq`lNIP)-gs+fxf@^a|9A6dj%ek97I$`yu1VuQF)6 zgN|pXZ2Zg%nfi?>4r}j;Z2y01-G7h#HKaE{(5?sd&WH-Of`GFYA60uHbLn zisa0w9SQ9GenilEk3=9`J;53c=jVYYqFvBWJi19;VkLYH zO4!%jf(EWh1ixcn9RG$!`j>75uv9L?Yw=()p3#fP^evyN5xWGMZaIkxcb)sapi9A) z!dYG;?MOhOgH)|9h5ZaPh`5P9iQO>`15?9Ujy1ufH2@qfZD(_FWMovf!6i3@ndcat z18+`GRg62X--&u|e%+7nx+>cTd)D9LJ*@`4Ct;b!?- zvMnTz!I3078FhK`_YHBI0-}PJ%m1=e=TL74?*X>1dD_ra5YiODV9^j*ZV}v!=qw|e z#5+)t=%;Pd?GCy>+eZv4wV@iEqMO+ZT?i-d# z+ef2CV`6Fh+p@Wn%JCUrU(xvrhp8ssllXW2>+IxJB@Ote&XTj>6d&8loMAM)o8^%{ z9M73>M4yZ_gVjc$e|U`0%y}jVihiJ!e)+#PdZL17*k+k6yIC)RoJM&H%qqj@`GNGJ zsK^RPAJ)K)GWS<=>oRT78-t>%Ge`s_=PfRR>$;=y(ug25Us}wSVHN-9$>^xObey(x z#H#w%0H38yqEM)PbqPn?7qBJDg^8JR$f;mEb$#`=Lj{vJwBWEV5C!KZtUdNE?JyB# zKmw^1u^DDS2lEfche!g}ch>sB`-evd5S{`~2P&r7dP^&CS*=2CBV4HI>j($U`|d$zRCiRz0E5ZB zy*!C~rjR)*ju~B0|0sG&W_R?PSW|<5Mf0-6MK;3DZ(o^>K$GMtx%{>$F-bxaXmeY&3tCvCXLoF3;iO*fF9*Z!@ zwo(ZChWWCN=LnsWJVLj6&r%3pE%D3j?YEN^;&Y!yDpW>1wS+eZkIW1{l8^~GNF&0y}DA$nv_m=J%iztL+;yON2=jZvbQSusU zfBNHDA+$^k#5eoVXW0C!p^B*Cl>C*{+Ct;KJ>eWv`6}hp?p>3}_&Rz*A!s@l&X-wovG~+bX~yDLk>q5{%6*x~Sca4Z0R@`3X;`5x z0H4sW945@QHZYuR5@}3leb!TQm`=JOFe;s~nN&VqPp0UXQD*nw;Qcvj*jl}2U8LDH zN9^x=8J-3WLySB9;(3ycN7nxT3OK5-Hs)#xINTsQR zefa?lbZS6)uqJoLqtR`8*W=;evk;Dte*5&WucB{AkZ5{dI52xZjK=Q*dZMPqtWjfa z($$}w-&0M^+D!JijjQi;u3GAX%Yj_BQjO@A7&|GaJC+N$fm8X|elW)k5tccV_pBkd znB>c}NM9kGNxsf=r8biga{|(94te#R;ZDLBtvF;&5sr^Zk$(|^Ni|*aC8!X`dGzfY z5MCZ<_(e=GN0J{Gf z!_(VSiFdjGe7p0rJprSa%0japlNu8iV+)B4`@hL0-(OM`lWo}d@9ckfSj07 zaG6m03B0}@{)ASE`5yfFb}`yE&_MiGsZSP3!X9p^q8jGi;)WS7kVbmu)3PB?lOU{O z^5-WRmbF?1@!%;!+KX=NjLoMIF7ca)~ZRBk5P{ ztwir=ss6y!Fo&t!q+F!S<%Oca^OlwdSyu!}W5qDAK#!0QpWG@`R9f6z2WW-?yZ|i0 zvvA0Uj#A->BQq0@CWk)+h7HKb%)*rO!UnLPi3aTW+-FgT(FM>^Nq7d_o_NvpzWV7m z`OL0M_dHn&@vBUp09E#J)x4Tj7GBMeyySkeyuH=#`cD0C=fU;A>Ll0p5ACxm*O_h4{nUG7^9;85Z(qznmfZXCW|z_Ho-QV*jaxF1 z`vH%G)(_6TxyjAnNGNob`73S_Xw`BRAg@9{vSgpK^fGME=BOd>A};Py-)ih<(G0&B z3*27b;{HzpV@@2d^7wll&r)yRU^A zdDus=I5dfccvJL7J&GAkGI|S>ict89Zp)7Me+4E~=qaWo_uJsbCa*PX(I`#+%OL*v zyG=fjFNV*!3`cB5S4Ul$U@3S&{PMKqCRlRf2XS{PM%;bhp(K26V7^)z7iy#93O8ZG36wg=yYi09_a zt26cS(mF^gGw9sEx^6|YxOO0o(%p=k5?dhT#ZaE{0t@80A1I6N(39EZTf&U2rd7lv ztetN(CE=Xk0y$Tt$W6UpJmf?2JuXa=x9_y{0VelGJu7XaE$3#NJC!ma z@R4`UN6=t^DiQh+ozCc(`3kHUn|=z-zmqL*gIB6uhu5=f6j0yV-Z)sr-069HGN8V_tVU4S;jMAs|8~ag zrNdv2VbpBSO-+3~o~!vhL6iM_JV-i5P0&lRmJ~Sp%txYqy1J{QNp{uN1T5s$>*GR4 zio(9hu4M#U|CQRB-Gd&@9mW3BhBUrVgO$Lje1NdyA4do*RSd2(EWKW?-y1$#WUZ2; z;A%cWPk4)FLCo98C6_;8MBEtr*gMK=6XBJyeItFilp@3QDTc`CFiargjr2T{zG$29 zqzVwHvaC8}_fu!O*u6X9+1dpts=bIZd+(GuDwU$yIflxxt7BLh0VET4pwiy2C)oiC z0|>PnoEAHYg7?M=Ccq|@;xxT45VFpEwyC*8kS`!d6vmhn-0dQ)faVE8(jg5jIlj$k zw>!4ap6Z=Ee#45I5{0ghxsu0T&6#02j9eZmYPH;0PE3ddd3kGv$!0tDDb ziQctGw~NPcq(}O+mJEFs*?~vXy~k|*2~Xm9hFR<{m-u8;|KG?*)CiVE*M+uIT7Vs6 zJ1;iw8cFi5e7I-v-DwcdvhSk9ep`$`x|R#8#KV*(Z_YU9+=~^4=Zpc0Ac-TU0J?lr zvDd>A`al_lEg1+YB4*hI7i*My><-F2X}MD@`F65YQ+g)D?)NPR_C`r4a&hEm8=FuY z@dQs+cFN37R&I!4^^{@Z_keNF%~1%0{9O&M009C_il~Y1sDbgle9Dy%(fo@WS#K?f zV_7#ck6VEnoNubzn=Ak|hFbyiT?PRJ)m04!TK}2d0_+Kkon+cRT8Z zUu^?v7mFabpyDVFwE9j~l+c8k_V*^wZqBF26Y{Jf>{TtbTK z)b_2D%a9J-_;2Q9ooLGMNNYjc@pmQvOAX86h~t_2{k75eZG3u(NpWPaiuGDyGsdYb z$ba!Voi5rfL=tsnB9MMl^bT?t*W2**Tdz%$*K?w$e2|TX^W(nRa6X+bKj6;SE~S`odOVz92ZM*k7F*+#gvoj=B=MAC+-HE!{`W+Z?xqwTEL``b%`f=sPaJ z8LT?CNF1UV!J;2=H9hyuLWOWIk5~WpQF#vU1AUoJ4Q9-JfG=+)JWSK{=ZsRkG2Nnu zDNk2w(nnBss>`TQqBp)r3k)XYW{==oJXyf9{r`yVJ2>h5*l@Sf#*-2IU-6|l%OoOJcgy=s-T*Tk#nn03?CC16 z7w2}GQYMmf$7q#^P7S1h?^x?DxALSfS#=z6dOOh#0nb2;ai_A^6f&CXJQl)+i<%Ux z;5yR@X+1J`lt?m;l;;S2sfW8&+nI`xJq8|`hdA_1rF`wfPwjWhRk?~t=jv+lT@Opn z8fPUZs?qPmam9anB>q-M2PUfbYQxI#_C7RH&b%-Wl=0Hn>FJ*Pwx(_lovjpb$@(ft zKVoy$Dfh(k&-1l$p2Z36UdvSnZ#?0(8t}#uv{xxDe&xU3a^$^;NC(X4O-vo zFdyFPQ&lOKmM0jSATpJ5AucN?Cub!Wv?}VOQKF$vUty8&E{2;*Ou#%kZ++@<_4(m6 z=*gbbqZz(|r6-Fl`b=oCL65^e%~lu98BeaFlGN=|RPoi)Aw_VW7irF{xUt9}rq;HI z$#W!&dZf|hv5&^VMzx14U%Mj!+7nubp?0}A4!u)KNlRB4vN2A6ZZHX2@c8_%g&gW) zOQ&&Xg+TL6w(tMwlyf(wuIvD{(P{|?RH;f-9V3`lcIEKv0AEruU>G@y&w<~AR^D4^Oj6dZoHezm5PD9dhD*cE5<0$OV2bL z`eSV+R*SQK`R-|J@-)QI0g`!&(z^6%K_r4`A^CCpNujDM5GQsv$rl~{b#f%pJq|Wb zSbTCkhUKec^wy2_b(z7F;aj}RQot>!M@0)%*n4vMh-G(d6HXrW_>1>S`<1RuI-nXL zA*`zr^IM&3<9tH6+a>z6zH3{*$OUeB@vyYNCAUKYaIlP4$Vouh>m>zQu@iYygh?0i z-(t;vl*@e~DFw{AJk(pm4w9`~RS^-qFDZkXt5A+uGT2dx$fePWNIOYk zl+pKY0M}|P!(*9G;!Il*>Je# zh`$ARnJ6O|UNQ6!5(U{~ZF^?2Z%lP+J!AG}CTf|Y)Fuy%rD`mMo16(zdb|xNb}>Qd zKy7$jg9b#&lDC3GlgSd32V8-YBnbO#;sQeFL8d`}sFOsOu@99O3FQd@@+=}Gnn3() z@%ku;!?T@$@4u2RGt*MBU;!7s4ermupdxZ2^xzCF?Lj(HTY8}i?DC=0x810K^Gx^S zg9i7TnxXwW?oQI(fW!1vB|OPL@AWdWA>lW<5j2d?R1*eN8`FXNGUKNzo;{w4zj&FW zM&6G;lygFWIKMdBpMH$pqVMO}H34284K7P>s@sjUamon+hgL;gg{UZ!^Ky5UFgL0Q z@p*-(`mYatcT<>a4HqIcznMgar(WoV1mwV!l_XVEDJkTXmvef-> z%~+V%k}eO!h*Wkt>ZHG(_>dA0$>-N;?O5cn&MmWIM2LC^Sy%*sN&zmZM#%DG4Ys@^ z`Yf5zhGTYs?rEJX3CozbV|(RsO!&V+noq^boX9!c7;N<*2F|~I%76-=Q5dc{koeMi^u%=vhw2l3l0!VATBHi&YO zsB$L{$P}kqlk_r*S1NerZsok=aJK4>JsbO4P(s4+eRA+O_TB63fSK&;vEs2HBj>}l z&0Y4eG5=mVI&-V}>sMWWP>hBKyQDl>;ix8@1*mX!N<~DH9ryM1vmJz1QjP*3M1UrM z-Hyjd*Vh2ixm`dKoV1;6>8&aRw%ujmMKQGdTh7Ci?B58Q+NxiM(2)Cgp}CPH>mY+& z6M3f~C*O_F#UVIu(!}-9dfT;lI)B*V(x)bui z1LqQ&75D}GNi0X=#eXuIpV~^*csLmZ5J%K1?m@efviuJo`~xLAHdp+BQ}#7;^!cZuV4RK`abc@PjogkqS)k*nMCOeGif?`$um^%nZ53mTa5TXS>4%6 zy*AbZf^#`A4YBPd`gl#!DLUIMgNyBQB~->sU(VjGd!Z;XqsFblHbHH0Igk3>&23Vd z!tdYtu6sI{-jFGffW9W>CcuPUR3S%0j97fTGXodX)mUn%CV%Gdx@y}AXV33bSe%am zOH0>$9Br4E&3k@u{gZr?8A;1beUX*JyC{=@isT&ao#{Ir$W?stOMGrX(=iG(reTS& z7S*UZ=D;x(0Sp(8-RuX6oohVuN!Lg4?~WDhrwjXV@h$G`yKmm$*!N8)OQ4Duk>y-M zq&bb%p$21}djSoS3-hAQHxw?-x(N@FRsN;Z$0s`P1IqsE1wMG$tj`I9R|Xgx{0-yw z&o#*lzj(VF^XN>|={0O7c7GJnU9%>H314jin~knek9FZT^Y-)r>@KMV` zI%rD{a<5AF3QPpGjva~R2#<>=VB~b|*yoANHh>+MTJXJdl#oW_wuNW4 z&%6o7)Cew!h3}miDVic+d%MAIovA_C#xta|1;Cm)b_k&sW{fwRuPD-v26GdPL1enu^VZXArR>LR|GcZEH9HvQVmn6WR%cig z(I(DpjkM$4C_}K}G5PILL0$4X}%*|VKLAs=bkDQ7oDVq!Xnc#URT$!a;q_ICWP?OCy2Xnq9RlkFXJXfc_ik28EQa5gFgFI@_#4vkmKwpgqspBEOT6k|EUGEP8;pe z>bk9PPc4V^;w!o1 z701+5Wiyy3v2*e-=Sxc6k7g;ZS*RXD1-$)W`$NHp!A7bFqTAcd5C+tOhDcFMA(0QB zWsu~PF;Y5DOpfvh7gJ$CwUHmOlfSWtkeg2@%T&f-`jhw=~$R%@*d$PNld59NMERZ$DpmFReIpwPm4s491&;rmQ~%&U}q z>}IPwPG4u=0ba_gg1*~raGVp{%*DmdQQUqAH^ErzRIm}q{5GT0Xso9vJ=Uf)S4;sY zn>Mjc#&8pQEfKZ_-^1H8nw{g0jLQ`Fx_wc8@V<1MnyR-I-PgxZ$@;V<oIHH5tU4aPu$R2hbgUuc`{)ktz7Xei@`nf49e0Oh2?_f zFPW|5DW&T;x&k!s7FIi+uE5>)E!;2)kMjj2mRmGpb_+v}>r4j{dFULSZqgKg*iSAd z^WCEYap0G=-Cw=HbQdZV`2lS;3|HqWFc zi5-i~sr!1n+UzMYO7pJxs1(jl%Cx(eC)J|5cJL*HF9OgDw^)I=E~lBEKa_v$*3Py) zUS}_+a=dX06XV;_iuX>aN>#o;VkKZ?T=_M78T3DzmJbqr`i+FP0c&?Gn;XOK!=PGv zN{R@m$;YD#;JECKgU+|QyOy--Pc^tiycwK5Z`QX6fKv5?H-W5V!~C&~lWe4m$v)r$ zEiaSTWgLg6gDmUyFJQuc#Ak`R3oE_m?Fu4n{7j#IpMLtSVg>zy5JA1id8h1L?V))P z%V@oZigP7(*~u$+I~_~_qDD!sIp?ogI`t!yv_!?-;Y;;Bal*g*1Q5cDHDEMI?z=pY zE_{5MgP{SSC9RC|b*<#6Jh!&YbaOt{XoCJQc{igaP4~(Qs*5ZQ>Cyl{8KJY3uY(wM zh__lpov;n4%nZm+>4(2ZC*){&>X@*M3uHOO1+1u(8Lx(-i+SjomPoo`O`d=L3{2O} zgO;UV%Ijd+9KEq`d+9;a(=ssVke^G8fb{4A8SE2^-s@m z)M8Toh^IrArwi$3WGk;eUO_}YbE4<}`xcd~Q)%Use!CL1Zg!AA(&&bRW`JIFuDNNN zJ>MdmaYy}OEY4*8`4TeTsOy&e3aZ4clEuKF7w1ay{?S=-MEzuc(wvS_-IZ4Qkb*RF zu5D$NFW`N}F;?n$8$3K>a|2yz8v_*|ZdIvOi_0TFW#jW65D8!*{#uvB!EyltnZWb)Wm`r zN6A4!ElJN`TFL&tjEnI{E#L&y= zcn?ptRmBEEw2+Q6|SsX9rC#bM<=b-GMn7r3?*Hb!F2fw)hmyaE(P;KM;+i24#L9x%HKn_o4X1peD{23S%cyN z?xwr|%&ob|;8>mhd(K#lO^Fz(q@p6`$-AI+)cy3;dvZZ;{OiW#TC0h_!yKd5Z>!tc zK|Y-@iE%J$Y@%t3EoahiTQBDCfYYig!4CUXX&Q%M+u~)W7W~iwGcoluNin+LS1S}A z4RG0fe(xv$RewnZmY42{lJiibBzpVAkLhj|(~{}tc{UX5L^y!EJ7my9Qs zDF;lSf&#Ay9PH-_3g5)ali*e>c7RuYSBdo9af;ED#LUy!dIW&(aRKF7ALJGq^*Xm( zb+ZDBHU>enO)Ir^E#=68tTkO2Tjb@%NV?|v@V{7{pv==B49BpM9VcgZ?3->N3(1;B zQ|8AVOq?fVO)NE)urJYPYp8u}9zY-VuOog?Bz z_>-pbBq=P9Bo?SYjY6^TM(5Qz@p3u@MT_Ct;spqI{mi3`q8=Q*I`j>zU^)~l%hFrqkm-&4l}b6vMOn#kmeT+MOx1ZSs;r6X~v^ zJoalJp$-W6TIe}SJnThO^X79Xv`AV9zxVGDW4Opn zlY@kL-Ury2aMp=h)A zN`8`!ViMt8^Djw83I+p=Wv;Q{BM(X5GjC)7vrr9=q|-%R-MP4Lw1-oG-TN}zm;$-{$b$OmI89t;TaR)ucc+GKSExl+z^&N=IZ%n_fOXy-XI3RQTK7z z=aG7gxo7@=>jELjlJIL?KWyG(MedG|%}m z^>eHe-`9_hg?CB|eYrg{_9Q}HZq|$rN!q`iDiQeKqjTacuQxo&E3-cMWp>o(M*VTm zfXkZGy1Qk#N$6v<+eFPJy@7c4RQFxu#-ry@@BE1My+s3XT3OL65i9<{T?tnvgkrq` zqgtxCzFtO71RHtI1E(<1P@LB*T0gYxe-%W3zB4-KO{!^F{FIgs&%kp+kZVQpbcXx zSmt}^5CMD*k$KTxR$fWvzZoDo6hC4vi8;tKWqQX7p_VU-PBKFZ9l!vuvc9c<_QEr$ z@0C?ndOVf6Gz%t+RfyuR1i!y-R}#}Cr=fu)c?6o0$u&ofKG`W5h5uFj%N*`TEJfUzl?{mU46e)Iu4ZE>X>YOLz*tvOe6dss5X-lOqo;+|>xuP6A~yltCA z;XXZiNxeCH`*(jbOzJjY%5FRun}>oI%FM(xK}@Ru`2FaJI899~_SMgasY&DlIO?dCC_VYEmRV7f%nTDr@)g;a0L%fW0 z`qG8pI|-f=#EuGPqa!QYo`NFlBQ*^cwKxYU5PmAh9Z z!IyIdm7EM3XOzg8KbV%N9WX(`!Vr$Ks!1(i6!|+~M2_}uDYcGx@D~+rJ5+5zot6e`=Z{V!$@F>>W_$gPaGdH0Os46kDoM>v+M&X$T0vRx`SB+bqpg z!uloVJh5M_x3@RaYA#YJYU^WhTvwk0MWe2%cn+wPzWDB4T-06g#?{8yFawK^EWzpj zbkU|UV{*o|tEl+UI-ADO09&7m1kl^BybgDEZWc1;)iRv+xbHH3b%HC|*%bJXmHB1H>Rexc-M;0oK6#8n~s^Zu|^!c>yki*nkU_niv5UUr<)n^ z?X&9eSJj6XZu9$X_%So8L~}>Km`sg(oLcP-jko@{^Ha~)*7xrRF+3> z;2cBSn}^l^8TVln>#-~xRupUp&g*?%wx2y9R}g?})B=|f7kys=*BRJ$Vt$X|>?iVA z!@rmqbScEsI3qXi74Dt49MH{S`|9bVr)fQOdCY)Pa)4~DCR{>BSqeLEz2fg}w&niI0>WLaz$tqnAYb$w6EpN=yJeb7 zaR2}%9cNA&S#eY(YMfjja)ZKCx{BR6-kPIcDP%orx?MXon>rf=^8hG#BoC_rxQ6E6 z$(hR4y%uQWi^_avf$GmOJj*yBbsQ z`klqrm_STiP@X3yv>*DL{*AsuA3l#CcleiXCES^g*8o1sxD@>6@~R{+bk0jrT3v(= z8qaH={Vm^3=Fc8}n|ey3qNWXt3vv@!xW@2N&CN|4Xfg_7H7x`8O0A;CS9NH5y#(>@ zCkqiuuXikq_B`y*XDTC0W{0Sw+9OsL4#{*f4qpNXzL!n}JZ$MD@yUzf6Syb(sorhy z_o=q?LnsKP92QiAOKQzZx~4BylAFHiQ;LoJDLMxmmmO7$Q|foDQghbXbbTPZEASI& zQPRAI4Z-U;vnc9~6VvcSoJU2*@)Da}w>t3n?}PGBSjouc?bU}=o2vOQnE?*@W1gP5 zKD)Y~MhO5}d|!Rn8fL3K_}PgiQJ<+inXb{1YYhQYN+^6Hp+@Po?xlZQoHg?Zcjte< zUdDHbZ= zyASdQrpH)O)l5$3^na}~JRnOIFaJ$#4>jyw3`*QV6<`b2J{}E1tCaw|3#mS#-fz(H zbyx<%WOf}-@^0g!sA*`}0M%!979+w{)?3o`G(q;EjD_!6RXJUkE^hi@aB zUqkp`q`i`C^B(J3izXGZ^we9l#I3_uhHGg4!XgDh;Q9`;A;K*LxY*0n7|r8!2MyXr zirqHS;4(QP{1+#2l+r@>$$vzDKMpPpuIXiaNUQVwY^R~7e18a}dpRE66CmkK!07VN5=B6X zd2DS+{~NS^mI>-(OVYZcokqZ)ZaF&qrQhFrGb{EU2Ebn3sKxwpt5eDRbnDlDN{%Rym3CSZ-9U9%DgM038jY zXvvfW$R>UI9C=M|?GW(oXza>sh}b|c+@k@CcO}lq>!-xvYqEU2d6L{BNcc7o=g^Su zAY?2g$6z3A0&9{&Y@oV}ELTQe@JSETp2T#5W%W;jH`J~DTUgSsN`a-JFF3ds;V4lw zbYC|}c*;6^I**jaGXjQ*vs#{@w}@BQMRmz1%5OJ!HHg?rRaB;t+L%$y`4xYQV`djl z-1gZu8IU=fI!T^q(}|yNwf?iG`;WEZ?4`GMdwFP^TUU2xrmyetQ>4)r{?yZmoI^{G zvQG^GvxSa@a;!TKn-rVpt=>EvbdqwVhEjc=M~Io=#<`zsgSA(kS&x7SiF_KK=3~H5 zHr$l<6Mlr2<=~HJ3WtkbRgpHZZY26cI>jEZa4g3WpIXi8_TwPhT5~*|V#2O;Z<(I1 z6kk%efMr3fO>~GcUbcHk4^|1K{!LK~1$pzGnw8Cm8}K5bTItay?Q@fTX5rpdPkZ{hDe;s2KeMpgNx~GBq}>Xv##tSEhM0y$;Q4% zv2eo3i6p0hX_iBUZ@&H@mQ#pQ1Jy*W*DZz7cXHYh0lh*IXavZ8Vq)c5Wf$Y2n8!je zE(|_g`TE_>y@cgLyH9GWMQC97Q*gAzLtCDM890uxO_gV&=Fq&-E6x5ab}4)j0iY*H z5rr8$20K@b^XEaUs#eRh{JuJx2lzKR{~;$ME4)Z85`s8s{Sr#ATU5dt{%8dYD!?+( zR1HXO@@naoe0amO`0p2_y2j4R+G^KCFU}i~V^KMqN{TUY`Z`Exx!r|5Y1rvJ*8i!H z22p?_T2pgf+CSkz93Dgxt})J8$1eTJSB;3)HNgZk9~{DZCfJ@dTzvXa7vrOvs4a5t zf@1z<+34UCfOOD;JXrcO44_?Io`HOTE0O7ygWrxm{KQ>}G0Gk)^oDS z<6YcfucaDJW;63=8Im6;JU#HAD#UbNWZIhCL+yUG)aCwvRGo!aTT!>|6Ck)1THGnn z;;zLh(Be?6SdiduEl?nMX|Y0aFYdvDLnssr?(XjHa{0!4@80|Vfpf+vEHQ<%e+>E|J!Q)U%PiY34j&$*Sqm}yhy7y zj>RPWp!L!%HY~i91rZ8GoWB@$150dinc1(H5dC|9fTekPwMVR(8(l*|` z9Et_}V;`pv$9>OK_`(@8!D2NZ%V;9HD*<5!AiRh^$Trm$rIWREN1d7Z5M{SAk3PtU z-648sxckb2KxPF;6-_Yo=tqoUO>Kbat}}dY{ZvFg%WeMsP@Wx%Ei~vF4bGz~(U=aR{IP)= zdN+F2+U+44u2-4iXx#R~B^1=U0N}ps+^0%e&|r9th{Xu{9;s4DHykIv@Jv;@nrdCw zY^EFU4TWknU~mg3S#`wENOH)AYRyrY4d~`z^&H+V`n;C%kogp}!jBbBHysYW)t%9l zPRvrj^NBjJTW{p7M8@1I24aa zJOX9X#9di2aUYI(l|}kMT7I{`A&qgE!iO8EAGkb{ckA~`hr3y5K@xp*RT)c+-_$&)6?HEj7tz#@P)EC*-WpgoAZ$?$%I4S<&|+B=(q6J81bcZ-l*sp zYRnJ~RAi3&+&v*{_jb$TNzXRZ>}AYc(2c~(`!Ax!Z8 z(F@%|I*(lg7uGB)+>3@yfno^w+(;%oL}EG(KDGp+?Xh189qgrrOH+!JiEr7mZ)7T|XHPKfo4=-<6 z9616rNQQea87pIMD7R_pwD%(cF9X~1Ns^SPO8}}Z!K;5{+J}=H;i$uoB%I`8vXUU- zWgiUNfU+$4;~evZD6tpICQ|>vj`kBWvRKQC$@aVryuIFk6wbrIRe-=fwr%+wT2@U=vCPvfKOQXMebcV(g<19AGwNu? zCh1PgfnPoJ`jQm>`mEGwqjZwH{`6-sKEOoiU5?>GJl&}iGVW%lfng1o(apZAK!ZWWjB?@bHx+88HC;SFmv>2^`( zMH;Z&#$Wpo)?EMc%J=7M!g6%X+&SQnG(yNU!G1}g>Y6y?Rg^Z)9=qc2vW?!z4@x~R zzAoX}?QKN9qerImwPttcv$HshI`lS&J*K2qMy`Dh;Z4u75o3wtt*7_NdaQIQKOh8S zqt!M|UAC^ry$`VEgA?wDZIU9IYG7mzXSb^0Kavc2`ivBzRm1}A`7ffj;ykVN7t?}K ztQ+uE@MB6;x_#!xx|gohZxMGH%FhV(TzH1`egR45b)zS1)Gwo}Y&d}k`9(7Kt>QaM z=q{h{zw`L_3|z*D1Az8a`1+OmZ>(#nF_yJG6VwEz+XFSx z{v=O)&rCH+E~#!Si`>S+jR{P)Cnp8`1=w6OC*D2~51$j`_d{lr8f1w4o+X>ys`s8RMxcAF^$eq zt}vmD(k@LIIyPO9y|$j8Q#Yr)GGQ(SigGtidtKwSnWVi?w-X{C4}e))_JIlQU02sD z*hP-o0s8jy7WsaQ-Hp1RHcO!>CK+c=5?SWi=z%06cSDBxEK6Q&h{njXs0hN$41{@yQjEJ%aQ9Wh*&38(v ztiH>Z8c&l+h-rKly~QvhfQjcjC?i6!`UA5$%;|GR33i}kbrd|f3;M70JXo9weA(^vcr~fQ{S_4g2r*tvfd?msJ9X$9n6sJ#y!r;~SN?b_m~J2@Y}+6*bVFqkZhFWb>I{Q+dezpU%ht z%4><@hrWvYk1K3FB)=pl%hlVlsC%w2bYhPYsDYm)zuJ+r7@))W8MfX3hFnJwuC24UPq%9smq#HWzWBinT+L0%gsAbx?XfQ zt2NH}^TsqapL3BvlMM>u84X>ron^2JWSNE~x_hygAc*wd$$Clu3l=Hmi6-*ze7p=+;^3*jkmt14Pf=?| zndNu*mx+j}Kg8j|7^4h7;kGs%nr8$+HUyFr5~Y}!6KZ7w;&MOcbJ$v|yryRqm;4zK zcw`}-O`0rOR!X+G2IM)b2;k zy>Y3s=OBXOcK5C4O?|@Uk@PcSognQafswG%?dRA;aG23~;obRyz?-wNbfq1Vnd#PQ zSo-D_@JEfTNaV%K=Z>@dSI)!8L3`%RWAfvh=bp5-$oq`tPSN6#iSmVWEo86eKAfls zLDmyE#2%M&J8P-QN;+#x-!_t8{yO+K^mfU=O6XycMdCM&@9INQ&Xe3b9QUk#OL>0N zvjR4q*RTrW&n%sy`)|F}Qy8X~{*AZ<_4_BA-7dFZ3I#mv8vM@?+z!~~D(gG(!yQa$;bpo4~#y*G0d8u=kJ~VY2(L z0n4*LpeZE4#CHils78w4fEP`e*Eu_3fE5JgksB38CT~=J{#^Puri%Y9q7bO+@Jm(O zF)TX>N}@kk#RQqrrrZH^i$$$}CCk*Yzzk$$e1%4g{*{jvK|usa7C-p2s1unD^})M= zE<#-V6y_Mn4kY*BCOst(Pm|izi2*fvW0NBIO9d{$l;B@MQ{T zaGAvdMc(pFu*fhnVXW882@p*JZJHn<#I&uALddZQLonru2-j1r&Egd?Y@LlB1!)^e|@TZ>cI*U z;D4fax;Zl2_3$cOz^d_O4(+Rm0DH|B&io<*plgQCmvnlb)tGz+Xy1}Ql!Pgpc8wmG zhPR+xzo_vrO$5Eh>1s%C79h7bRQ9g76dENcc)O?stfA|_%OP?9>C?R-f)2xZQ~N-b zfR0r`qI5}BVe!`;E{ZoxDR0_E+Azty4`1fw{oyqP4vjq66&Sywh(&(*dDV>jGgGDO zJ1?Sgkxk!|CW~K6)FelvFzI{azu*rlXVQ(c+k2hOc~k*KTf!Q#(vjSspKR)brN}VN%4|K&r{{JUxk2`*;%U^-H9Kndj*{gU?fs=fhB86RUGKPvGp`Q` z|C_5Ksn^8G`oL4psGmI9atvB|YP#RQ!yp!~ZubTK#qI~z3NLr>Kbqaj`s!^UI3Fbn zk-lO=2b-JKE0@pIDqiEK-HJz6`1Kn;mUVduV2#c{;z6mO-wbND)O)A#0-m$*heQ*0Wp zFFLbWV>r-O_P?O$r&_M{J9b`hEHQm-%dt;xQB7EFNgz#lp{3S{l-kXzM=8|MdaiY6 zJBd{FWl?3@nMI&j2v+E;8ENs$q4cICRYI)xJRj+RK@F5y{Iq&^IwA`P0|_Y$M~%t2 zMrQ|dCFv-HO*A>w#x9a5>oJz!&}ERWqHESD)Wy!0MVVyEzN^D9tMV_HGso_Au*7+h zt%*mcEiVc%f7_ysy->M?LRbHDd-v)+?q$c}fFZGt`iG!{moV}rc*r)v9ru30P#R8| zvql$@=cu}hN~GmCw;xj9d6QM7vOUNtDDa0xe~OQLV~2i(m;q3oMe7emm$}UDuwCmj zu6nQAIXHv|fas)v1>OV{W9c~w`}Es6>#ZQVlR>QjibMZ`Oe_Jcs$- z@|(_#wyeL6d5-AxmBn{mK*?^7`3G9ct^TjXZ$7Epm^i!A6TiV>jbnfF%k-S)(6!CO zGETrT#N$d&I)A2q|7Z5kjfr{~T((O<@V0n)#IA=B2L)8KXpa2e`zr2b^p1knw_o1@ zEs@DN2~+ViOLY4)cFKY}it?rlzf~>z-9%dUl>NP*AZE>rZYH*S)Rt9>td`J|LqS)- zET^o~_%E8-cZU|=_x1%3@=NyKQGPRlIz*z;&vd*Tn&)(hfy9()>0nq;yu)!gc#W5> zmaJ|T*{duz^u5vn)HeIMTijzPV~dfD{`QPJWja8V!Y31QXcLBgoQQn9;=_B(@@mhIs@ZlLKziG% zy2|_MGfuMk>yZpg3I2WuQ+^z`eM|QPOH_e#_cE86YTs6gSHRZ$K6I?un9wbUt0Ja(s|3s#4G-|uZiU}(80>XFHAGY?w{2n zf`s1h&m)s1Vqhdt&AkcKpayJi0mW88w>RcH(99m7kvZWC^5{-e(w|&4#h=H%rL_s5 zm?36S15F>7b80M60JPLRW*R_&n>am%!cru3ojx5*SVH}uH>-S>r1Lq_*Da9rAX5+h z)2=~u1eXhS9+AAW-ddWo{HibIpq{NpJnI`F%w7}?B8!Q9%PFA9`*pIG_HVlG*XFJ- z@cqYC2yHE^IqdT|TIBh_BQPWRFN9Rc%ig56(_Opi@^EwTgeebb1g>3>%UZa=bUK~_ zX~ZVP?kF0U{t$}zq1rA&{o0l8kz=Em1P6Vflu7DUgecE;AgKIrPl-l?M?}nLDn{hC z-0Q!7O!*XoR;v ziBnN_BWy=Op`>28&fnziz2yz(bSO{BV5~%bs>< z=o;*F8yOxp+KG{&`tVF77fNUFl6Wh&+u?C#9W>O>`Qlx%{C1X!{YhzW(p_q0=-K9; zQ2v-uK1ooBF)PF-)aS=zg#O*W*XqDrqe>+MPfxM@;Pj?)8L3C+10tCIe+jg@IgZ&# zxvG9#qqL3VZrew!g+%+E=d^MutErJn+|5|=z3Gl(gU)?(D+P<)heTHf@yJ1uT}tTg z(bUrv)=QBqR9VZXF6N&XLy~Pxdno}#5d=z8O@I)548F(Gc^Uvx<)A1PIwb9sn1Qxop%NoW`xxenh~rUfgQ}Izt5O2#_qJ-$!Z;6b z&=V#T|DOCi0;%V3Z!7~|4*=qJ8oca!P9*^$dsxaKD^UfkC^Ve1Z8MdtMy#eSE8rP= zEDGCUY{hfBZM}aW1rT~MOxY%V@6xdPKmM zgR{|(cLDPPS}gjnIV?A;Jb|PLD=fmreiszl-cLjE4P~x)7b#V9yFq1%8@;C_4WyR1 zaW;-L-O|b4XHtWoc zv|KI-V&c*9$q0RX7JlC<91f!u#X7A-rja3c?!i{Kd1~Ld#UFceioUF)X)~iyA9!?V z&a*{fY#&h`rQ9B2TK>X_ABjv2{)1L%^xpp=s5kjstzsa5?`tQ7?VWPS4ws{0f`3vL zu-F$p+Q(-*I}8Qp_jI?UX7YHN=x=u7|1jn6d)>0t;JYQZBr>n-ul0XsPxntVC?;QT zHvV>Y%6Z?P?D?2n%b?pHwLLvzp2UxKlS$}c)86Uny{Uok+AnlEM`gEOXwxCPkOVUZ zZSOq6Y{cvTdi$`ifvUhC&=Q;MZ1MEnc@+m}v7=t2UM$nKpIFd3$$mZp2^`}<=O|Q? z)}}-~6VaR75r_8QGJJJqhY}3`V9W_HL=BAxPVQM6{zlCQVf|isPByJ_z`C`t1yQ3= zT~rzXc#_-wpjNF^;?TGRe4Rj%nc4-f^FKYPBtR~2b4p5WdA4_lB3-s}^*rS)2Ik_p z^l-d4jv-|S@+P(h{kM5t+M!>Fne9nQG&#zpeT-HYG5Yl|+TqBPc54@7a*|JBRC|uM zOJgiuC~1PLFJCJf1s_bTd=PA;gifI)aS;7L^4A*x(in&c*ZMSh4uzM4c@=uIbV7|{ z%LHB~DVF1K9Fic4`@EQg$*5?e_QAY)3q3Lkreagy3$w<-gg)fS;D8OA#0QnvBLFE4 zu98W;OoFFo*29rpSW20lD`Jt!)17@BRRKJ;x4E2tPB<0 zTmI%?05PGJ(tvwo9pq=}-06PL%|4fsU&4~Xf$Cdoe1Pu5h)`HSs4xfZ{mMZ!Mg|Ai zP$6j7EH$YUv$2}n;OiA?a}#irO_*miE(w<~RCOlkmARJI><>Ip`kxZ&+prTbNH?AP zymfcqLiRTXzgfv$_w6VKZ7Wy0h(FMGA213?0!!s?mBcyZD>I3GWa&zZFx;mj?PlGz+A?|n#V|Nt_tI35x5Gz7+WOByKGxyC{B~<| zl62Pem$OqpBky)dX&ZkJ{!-75KkL{bR;Og+d?zHp{NXNGVkYQ;bX*6m`aF!of|Y4q zI-ks#Ugh+Q<3K(aifw_?yQbffMP{d~T+ldn7PYsnmLHp>O z_B8Z#KP0I$_@75z4>XN^k@P%nkJ2i54qH&om%NFS^vaR-+kBXuiLb~>g>$itg zK99fJl%B)7+Wgoz9&&H*2aoPmZ+CUuQjEWy{L6ILduVC|J*L(G+NNr^GVYdce6pl5 zc=czkGJOSSMfuNfQnCbz_zin=G5uTs7HecrK%DC{|N0HU)YKXV95*oPh7$){{Q-;? zFvjWv%3e-Vm9$G)td`4&KJ!HtioYCxP?yahm)L;jjW z&$Q76z-3br8qq@^8LXRvij-CJ7@92N-q!V0@zbe5wKpl62B5l92~eFFi+OeF{FE6D zJ!W7K#fWO-Pg*2g+mUn<2lvL~yv>)sXIJuB` z(EG;%zGUQbU~#`jD4`WJ>w@I36mXwW^^Ksl!{3wd-k#W=h0%kKN-#*EK9Xv?+Y4t? zo5bGh6`Xw3a4Snt&w2)?EfqK#90x-ne{tZ#W2+{AgZOLXzGk!GMH0esDO)ag+1jo5 z2=mdF(W3NFLJLW|am;SXtU;ko!tq#O_m7&kFa?7eX)b&--hWjEUELGXN^n^QR0WWjx<9dkihRZ8(<`Lfl%L+9?Ds*dRBlx%abZLldyGDXVXdPt zV(=DBGlwmOO-o3i2$57k8^cCkt9@%ZBJt{v%;ood}(2;!7|6qRk>dzD2)4r z9FG{{xXG20-R^WHO()R)s!F6O3uqd5{`^N0N>$B;)Ayr?hBJL%DQJOvIQ1N4`Asd2 zR@Hp1pr>3QHsJH!TZFq6ae=^Xv@#@Qya!$aRq>$!&Tea6UB;Xa*P)feD?=Sr&?}ol zbdv7^PG&0hfsw3Rtp0@7M<=?>^zT`QOxb*MD_fYvdD#mclo*484;fD7*&h1b(haq@ zDa(!BfiMN&3E#GU1wJ7>(i|;bbkX4y_l?!bquARqW_i*RA11QNhm2nt=h5 zDjcxU%YX7Y$-*-iN6@^o1Vs`QXa&GpNEh?sS#TAt3tPG$H}p1j6@1vUJF{^S`}r+< zpd)!`i)v4yrKqXoy|K_J_1S=n|Ii`wR3acySs0` zpRPM)1>Bw&NKzMkJMvjlDp1Q*)xEAHefM}m`*dRPcDFm~p+oY%gI_|zPl3)V#yw!s z00<8pl)Mg-RP;X~^?yOJM%%Q^vp-DJRm=bA*@{&Nstep*1KfKxbYI-gJ$Di`D&`YZ zKQ)K>KW_OOW5By1>)s-gCM5M=dGO-9DU%A1#GmfZG`{%Do*ld?6zJE@NNYc$&YVG1 zv`=^WJh>p-4^B`GKmc4ld|@5Bl3-+HXC2e(I>7j;N@oE#OB-FL9g5&i5CWn)qAZ|K zFQ&aBwvNhDGBPjnv~uA+h)h4p5*F-orN`Ze;O?Us_JwbN%Wd=zaMuH0>&rUCRwi;Q z1%8G`lv-_Cho}<5v_aM);EFN_+rn$UV0|{^EK67SXj#eB=$B+00)TA?ab`-7W*>X; zi%2+tGeO@wv4z;VsZ|*HDf{VKMLp#Bfip zGzrH>Z;?0QQkBi6SY((y=Ie=dXybOq-XiqkBJc?PTls4OiK4=9roT*}+=Du&=F^l>KB*z;S z_!WX1$RdE0PmW3FV(^MdrzWQfW@4TvyXtRUwdgj7)E0RH9FjHfTK2d32+~!KB>aiw zfA?a6Ju>%jBQwJ-&_H=;p9%qcjV%qhu#Op(eA*Z*`+4eF@O8PWg&>P4;HruHfu>`< z90e2Zbi^P_NJgm67N^q! z1c~*mgJ^cS>OKDbiCmMElB{oye2sa>@uzG7@P=*9nU^6+TZ@e^!>*ePmvqek>M?BW zMu{Nz@-X|WF=YzcB3XEu{L=iKsb+P6Iayq#gWjr2Mj~EK(W_gVc%MHsM(TI}bhk#a zvjmx6EJ`^HmIy{|QHf!$r{9yen1_WAxj6k)eto8ruM=6ezmwQ{p7-0%QS$UxbmwCj zt)H;(S?W?mM|4JF=98rf$X*v85L0o(Jp4+k@0uDz>!!f}wqQq=_I7v(R~}RoD8zTx zlAa1MAxw3e{g6N5*#lp6Fc=)%MCjc0oVP7_HqPy{(^+Y4jd zDb^mk(~eJMGowjBei+s3xQ~al`$KKIVas0$A;%K<;Fn^HDqY$~mZ90>odOnb2Cj@5 zw{I^cfD0$f36SyjJS7e~x({W!lxxH#dAuod@?aQl7K5GAKTyEKr-pYo-k)j7APuv^ zx!hTIoH?}CHm1R7!|mU(8yWDeI4%hO9`YPNLk)m9fy}_Xh*C``=Izjx?FR!7#XqW5 z#w&OPUp2#6-AWlhvCwz8pO3n!;S^6SnBPoq=(Z>PdR3?jH6R~xD;RYB62lD8NifZ^ zunro?uGHSOYv47hnr5bQCO5=_BAjeqg#!_PMLhw<&)&U|IjF(~EYRBIf`OE5XqAT( z#!Z!!vZA_<=^)Or(*Ah6SAk(o6&$`0u%mTm0OUqSBovJ)N~)?&;&IPPa=*NCz)&w! z?)cq63HzR3WrPr1P$U^|6%?fB(a%yDuP7)Xph@G_1-t{AYgxI?=H8EFw}ab2;^H-5 zu=0oU9`G2bJ^@S|V z=SPKC%`@Y{a>Tr_yNE)kzM~(Gy-;>~Oj2|?q9jO?uTFdWiibDo@?4_nQ#nUJw`C`4 z;!@B;fiH@PE}%b!1$u7k_GdL$`i|03X<-M0D>B!_`zlCSEjuyaPg`G)fz<;D zLVVfzU-xjVE_p@5M$=8P>%~j&gCup=1Ejgpj->d5o~Z9o3)IqHX%1ooNepVY3M|f< z=DaKsmK=9rAn(4q^S$_aHFl}{`%?YN z=r?V5t@y+FSo>d^NGRPKm%DENdj%;!^sl}b)ym~@&qZRKWgc4KLLI{gLPVdGu?UJS zhc5gw(f=k9b$(MyRfpSTa*zo6VDuWS~RejLIUZG9$Rw7X@D+H%&yeb;S z4tX-GkcSzlI%AS-Iw!-NO>3?V30?Y_9zEH6m2ju_bguRR;N>pn?XYL_COc#Hvm{od$1`guchInlTEHEYb9xqSY)Y<&5 zA0Dil2iol;T5ggMcHba(|9*Bil6NlAdM2w&X9gT7S#P0vh7QNJ@xRlc?03+80`LAc zZ%mgI2;nu{Wxp7TI>ABE8Ehgz0Z3j}DC(x$#>Up@d@XG*-fF2M;;=LuV*z@gA8?~@ z9%E==)OqI{AZb?C6&tT=F2@ZXe)X-Vv~N-L8t2iFb$=3>8KB$6g3es-V9lz}0F0%Z zh8T|RV+&41mh)gXeXEH0`x>KN4<*V%mXw54Mr)T~6fn zR5{{1IfrQg;Fi&vN?~LZJB^VXgf57dsO-w-p${yz&%U4rc9sLPtnv$D44M-hpH))U zh;SUZH+BIxb(gZf(uBE)Sgw}tJ@;Qlm zme{Kc4Z1i95#e~G!pL7Jkib~AS_UQJfVjg77CyBF8}@&#KdX7Pe^jl<19q|IR#Q{# z5)|;K+sy>e7Ib^R|BYrmu+B`D&@RB9DrjsfQ;j}G zg|E4PMTNXr!FFK3lACKI-GUKF+~`qATttoYxHH$1U@-26&<%yB0-XFYHeW4VU-q&8 z0$6xK_>-X|hkG?aT%&g4S6p6VJKd}Lx)4(z}lnh_)p;<&v> zr=J$Ymnw6pGC6Xq=oZ7?Q6_juznhlv50_9h;mw(s82Y|a>j@4<{)YH~E)JCC6p|t$ z);ecQ5GN7j-=#($Nnc1=S5V7@UhnvHKO>#q{W|noUMMQ_iNQk0Pq4(w73W#Wd(uGy zKJPg#6kUl9_08UYW7XQ?VE(cjci^S)@NIK0jNQQu0)q>}6kw-*GT4LK<1 z-O;-9F-$GBr$OiB;xpkzN;|YJ4WVSrwcBy`*6esXSo$d5VNc)RqzM6OVO9)q_xaq#zo2lC~q1)IIs|w(5)=D7W1{pCTVJdR0 z+lUwWeajqcYjD&ULg8e(>^?9=o>zYwz^qa`-jUVVk7&KR*fzRzcLI6lrHDl$Z<;d{?_ z0DQpH(P7~I^K2~;^=7%pS$OxnH$zjBd`6IJ-nn(*{W`AfNE;(PGH4vhYlsf z)F(*>9jZoD&%IqPcek2VV!zh^0e3Z^m+$n2y+A`d`bdnHBmbUoS#;;*-smvwa;l*s zq*36+*d}V~G)x-NVAX7R>su<(^Kz{GI_F*H0QzoDuxV~yg14B#sBzHA_Ypm@Y^jeU z(FxaY8O59Le(^i+5(y4YgMSc6wT1;Hf&DPVkMXa3t%9s#bkjQplam}#t>c3`-9Bbl z8(s-WIfQ$IhP%rj3vI+4*0_wOQyaBS%fIv~bBk02y6KP6n6AKPTCC$&MWU6g*Ck0v^mk8`OV{#5 zXt(JI)tJlFwwu(j{;Eun^RC*N=R%Q$!WGtj>63=M&I2qiaOvq`DOy%s9?UjoR1@FZ(q^T^{^P?24Un;vBNlF<}jj(W>d}_VeJ#S=#sY z%-RC{pMhWJK0U|ZU3*m`F7~Ee2=DaPbd9}joVMeK4*gedkS{iIzy{UPeC=ZW@4DRd zk7qvFA?={^L7tC2(eKuRmPAyk+RmnVkzx2|-g#p9wE)%&Ri{sW02piLw((bD_Fm3^G%PK#)6>2Qyjq8DJkG;fKS!n zt`0TSs4*el_g-ABQ?tp%8B~=~!RhAcD2TppO!hvGdixq82YjAcnC_~99&KepWI?mc zTGkN;820VuDy&)w%LqkaRRi9^g@VJ>2 z`2wMpUq>ji>Y^lKk5 z)YsZO*eM(AaUp9}EO{#I@~eO{&ML&W$UO7pGwqr`DDnZdRY2BP{X2x3;WInQ!!?(G zHQ86EnGi1vaia-Wu?Z{8r|~oHORQ}sT4vP@19c>Zd(pBQ!{SIUu>p#0r{23o9)wyQ zO_i{h_G_Hrc?oMWQR%#XXYSdWBDAv-Ihp)LJ8YsN9EWZ}iEnR$zbVQx#fJhK*I2?;21X8#H-7%RKKfxy(4kQ0H#eQh^4GPSL%@$K zy~mict9JrzBklDBi&xI~qn7tYTIHvP`z{q%fKJ<0F&DCL&S{FY9V}xH?7h*1RSvKg z`U{2++XvLj2k-y92u<*=z z>L2T}L+S@(-MPGb+W!)ppypREh3;Ck$9Vb6iADEg8IPlLU5n;R@L!@_abYt$HUK#UYEK45TW}Nx7UHODX*yEQ+Wv~bnBR1)V zkI=LN)dMEd5!Kz^8Gy;77FEu*<8RXW-Bj^P>yW+#8c;D!rc=UOsX-xNu?KR`!FQge zDcQ+*i`i?(P5zTe$a8K&#y5tT%ri@4hal*A;etbu$Jc5DKYL^Up=#sjBi<9=V!?lW z(0D978c_Fat~frk00yvK;Ncaw6EZXZc=nljF+HqumBXHZ{)eE}kRcrB8@epv5qS!1-7`syjAVk+4Zel`JI#FWky3CRk@VqWmONMODQWP!%x#%E#- zTkL+@jBRj%umEd6;wyhJ_>p&pPL(Pz6KA_!fsfv+!-P2*!)v(tyNo!dF=A0{lyo#W z(d60P+U(7t^6W0zxerm=nH{jJe90-sX5JVa6zM%@Fcv5L8~2~Y!7?ES<}0b9rTHwH z(D~V%09&yJ3qZ5Lv`TdK3lvqLO+KRodUL`XH(~cW`Uk*x64VSQi>q+lb`JXXMKyaB zdqs9b#74V!{r(t_t3c zwrZ|pO!7a_k;1Rik`Wtt7*<8mb<9uy*{Yg6UK#8+EI-=aACT*uhz)pyBtxw zC8}J5#^Ms7`?q=S52IT{a*T47OS>jc9Zeel^Y-z7!PfWvA>$(7eth_#`!2ETUc1U{ zvu$|n)c)>XZuC3H(a)9U!Z~2!gtViqdPz0e!1Mr&bG>8M>zBShSi%~=A7t{BPQon) z3t(7P#XfG7UL9<;jaa33yAHEjlNk1W-Guk-xfOV>18NeNip6}NKehK@ z-m4OIPwKSj`9OsT0G4i*@ohIz8aI0f4y4;v)VM@WvQ%>rfYSS5srQD@*G@WVAYwdk zRt0*9Q?ZKHl|^j&6F{k<0S!Yfh?wvzK>p#bORGS|agG4ebiXWSCv%H4edT@Gj&{UN zczECd_zMoLw+~;(Ws;IEYEBg-BZEv+FbLYb>gK6R1fu=riMUOI!&RI-%Ij)dVmQ;9 z2(vUMwc)xt&v2pcRV%2$-q>F+6t3EqI>&kCr{gRC%*XDhovX2iv8|CTRdgs{UeSqx z5Mab|GPYfSYKmf6uIyl4h1VQtDKusJ?^sZ%Az;5u6AQyW@vMmdK=TismZCMgeQp_p zJ7M>4k7gilkLD~sb(cl*ggJ?!BYH5B5GTSggY@3xZxZM$(K?!JXdL;)Y4cP?OL_0e z_H+8;%O0C64`|(#QF}uH?)4sUyxeJ+#)86Oo-2Lk65tm|)FDp}x5LO94V$W`8+10R z`n8+TLh1nRqqs5$6Vjn%r7P5xBxp;CNQ&bKAL4J0L!R_j6IJpK0>RYj{V(fOl2wD# zs%HcPGDSy?8C3fRIg&)^)9#wXAbLn<`vrW|GS1INWyHV5-$m4DD*-)f;jhD`P&Gmu3A##NJ(~+HMXZi#sl-N%JOFP%G3q3 zCYul~ggqs%1YLt3^q~f8L#beXv^b}F6c|@Mld2nz!+wa3(Hrei0nGzr3L^$8U5rYI z54h}CB%SQc+sb=2fA!SV5Mx=hwTCHu3A=kMOhJKmH)tPoc}4$gHM|HBeE}&>L@TzG zmytU1p@!hV<;cLx5d&O_VQMGlbDK;T6Z4umy&kXXDiPL7z>T!>U#^Km%=lim zIM6>ozZQQH&N(e6=8Hb>d3?EG*rVk?Q07p zi6bvoeGl>|um{KuaOhtA#&&N*2W;ETD-oE~hYgK~1oUnwBriN%+8+zVv;^tWP<{$k zi81S#UqV+W>Ph-|VaPVy*WRi)F^2${*yK{7kAld$c0~Rv;q8Djc{;NHSrpo%4Zj4q zLC|j)C`OqQ1B>Q(dE`KLT?+9lT3vM3nt{u7#;8dZJ>~7nHs;i>e#fgy+>Ws1!jIG; zpux{GOg{R($;msg>kS320%IZpOl6Q$oNfguE4&9Cii&ac=J#IFnLt@NSV%Lz>Yj~` z==%kC422g$-HdvCf$0gr#ceOD+aTwt!S?g4h#<1)#6k$@7I=YzTs_F;o%n@haujIe zjjzh5hUNkRhMh_OqG}&_U9odfCyi5|FRTPLDEG*sFhUK^vkFEQOgt92YZVMvD?d*m zRN-ygM{)tclQ80yb1;=TR+fdWju0?+*6!yKhO&L#}^~XLg6^ABc5h+bj)!dB zfRUXIE6V5(o`B&F|2T=|&MY99qeSeh8GqEmKlDrSi*C^x@=KL`AADbDp;Ih!btdLd zfxWUm0J-$s@mR-x;Upy&<1XE~Us$&%h`Pjsx4$%eA>+KwbNS6kX_7?S zi-xEGi3u@;8Z%i;y{?2Ds2%W@-;-DCNqx?%ke}O;Y;Ali&0QJnN`0qI{Lrgxov%TH z$%+cg?5+fSqn4bCfqn`%q-Ud{P7z6@(`BhpCE}=|U$NQQ#xrUnXmS`6lpy8LXhdBF zURcWTxdtV#75}#Twn!A0=U}h5x|kxN2i`sNB!=JI%+#JQh{%*ea6lvIKWm=ND3Vx! z^aC^6vh_w)fS$pJJE%WTIh$B_<-!MZy9N~F_Oe;kT6iGg-li4sPy>TE;Mibd+8d9N zr~V}e_d9HU$g!&pw3~X08Qjl=g|d|cRZ7qXczr_Jh%fg3&>qZKG$v@zM`&4&-`=qz z;vui5`)p?=D&qKx>Jm@JBz!WIejBqTzFUW%W>7&?AJR;^%}ejpW~1Ows@?htaRhVj z?pmL-%A!7EM)~>Q`8bqNmqy#Ww{FCdR%l_s8?<*Tp33y3Bm)dt`;KfR3{U#~N}V@8 zD!xLQ<~`h8qQ(VDb}invEd6t#!f)~FpJRQ21=l(0Uj%#o0=f}L7E}s>-q{X^(-GWEeY$jlYPjb<1hxN38J#Hoyvs`p zM5)VX#!?cXon6p)&QV>+GuKkYvkofFPf=ZK^R`L=SR&I*R;!I~m<_lWD@wQa8H~@z zWfzK3TTao4m68Bv_~^=lm6*%qjK8)frFnA}Hs``NZKvuLVf8Bv4U!F`5E z?xs>P96bZza@o2AK#`SEZ?#&#wRl>eg_$qqEr_grL`9)gbr*~-beCikK$+BkUEJys z=P69DPGZ_*0NR5Z#EJK0dHYiL;?6**;jpljZF-r^w~R%+`uZTf=k1g6e1v5LZq^7G z7iA*vQp#6v_>eI>aaJ4OpD_!m;8XTets37et`Hdchnup9J(U(D&e?Gr5ANj)rQ+_d zRf)92WM?tqZC*5C#P820_taz-{=JY;i?_fER&vfG>FmTRQ6?lQaLi*`1#+#}*K~P7 z0EKw!K8sNc`%iw0kX4wh$s~#6tUB{pn>>K3y?eB8(=#Q-AiR)vH|?NW)FyO4%<63> z`?DZME-TXSPiuWhTb!KG*Y(F|CK-W5tm@Jen01suWf=xKv;PX8n9<)A#yLiFca8Qa zcGtA=Y3ADFzGAD^Dyk8tL`=Ype42tsQR^xEN;MhWrgzy*?-eucEkLt2_lwTfYhKt7 zTn`PMP61cKQXFqQL#>wE<_%Ssl4U=m4SSqDZc0t+LNF@%{7}DYUQ4!ai8+81r2U7@ zV#%1OmLvmp&8KJ<(CS2>50OjPk>~%hGo;p6-Dfy`5#@S1ENH=I00}%hT57Of`@T|~ zbp#%{I0dpxTMd$IZj>-y%zk`zuR077NBbT)CFU@xdpTvk@XiEF3ZKj&F?9s<-J~lh zL>~3{EOo)c36wNBFd%rH7-V$i5f%#<3);5H^ z5EU(_Ua0xCb@!z}1>yA2k~#ZPY#q`{eRZwsQFv5hu>7QiW7zz4#DWhf*HX?Ke19u< z_u~ND{@@_B%_qtZqavfjpeG8IT;|bxAK^)0yT2?m!zPP&O#D_lKhe!O#eCwjB)2o$t4KGYet!|%pv*=5 zwm!`y4JXF#FF;oL{Wtwt2D%%PThRbm#;&(o4Q2)7gJ}! z6=n3k>zRR}QzVA&knV;-x=Xr-ZX~2@2nlIv0RbtI?o@{E?k?#R3F*V{ob!)Q@UFeq zyPvwBecdYnv_B-iqDMdet_Y~)Zpa5#;s`ld2(mar@@hVLKV9(LAmP#3c|%i^jzbtI z;F_KX#sdBjgf=pH9JiaD3LRrPstJ$_T+c_sZq#&E| zFmAHBl{&s8qVYl?!w52JbEwHi-D0ufmt&1G5ST9stlSRx^-|R| zpY);71&VVYK%jKX;z1l&SP8NLuRi&oc`_ zx5H)^7<(?^rF5{IQ#cFxj$ajF6dH@~P z@2l#s-b+?&;^UUHlZM{>EvRbLn!(FV8FkKStj(_{I%XRO`7qyj$d^AXneujA>m`Z* zre5(wcS)7$)W0ePpLVK<%{ccu~pP{Rgfl z<>8nE$!E!D!9NaF{Zq|hbt9=^4Kokt%&o?UG9Kq6XR6yi3%EGgAIYIaE$KxYrUvHl z43%1hCuN^%+;@0;)qA|QL}6?|11N7FMFXQT7KhsM7)t+LgQ{6J&^l6D1gehrnwD(U6VCqNs?%vg4vm%e5m#|<<<9e=UtJAohdiFpY?I41Rq(P@fWg8-%%WhAaok% zXX-)TbmCZu*Brbx_PjkraI254q_l^Z2Q>io+TGeVfBKCD+uKzuLh*n#WO)2mv}6;S z=*z8ExTyB>TSXQ;PYPy}3ng(`X0o`%DIZO>o3~<-%}2hI z;c}n_F&VCrk&gYAVw~>**Bce5{{3hc#IE7Zi@D5u)|w*mu+hTjHU22%QBj<={v>DE z9zA8gnVjm=jM`pcL-%_N8$!U3@ zPwwXc+^DqPo{bkBtv|Ua7&^VcIiq+^+(5fk5v&ZV19Dx_1knnp$(2c+`7@C!1^%$` zj2VMQp&isvwVeI{!WozDInu82bIYUpDlCNArfGyk+LWH%cE>1N$+LTo4m#EWZ;m9c zNZ_5RaO`-*x^?ioPorl}P1I|tDMTF0gOU|IO-!fQc!a2w-I6v09@<~dbp>)pQmB5; z{&neyVEfvk7@8{x`rMTJ*DG)=vtJNXVqInRujQIk=KDrx-l%Dl4~FMmAyd@JHXAot^LUV}Rag=n!43x? zWyEee*fU{EtB`6pKuo^XhAZ?B!wC&%Nt#Gj0EB&@+00K!pcMTyq)H?TQQVuzv4jh- zXoRgp*q{TTS|ch=v00tQ0CArcA}UH0@!9fIII*L&-jV@bC-UBVr103$!;3Pf3bs>5 zc)2D~eC>5jDO>KCAP8?+ENEBdOLh(xV9ZC^@<#eIHQE>RXgvT^49i3t(4z)S_uXr5 z0I4`KuFY25*LJ^@cB6zYLw&lviyCRp7=t)bBo8w*kdv2nKgtR;W5THQSf+(W_nNQM zcpYgV97n~2KG0RsbWYFJp&lev2QYgq(j>^1`Hnw#pmzH)KVM{`zSUrk1FQ5pW}y+= zAO!`|%)v+!y62krdG%xdi|%s3Y{$%I2pr(KVpF~%w>>tYs26B%9G)s*k|UA%6!|C* z`{6YUw+JPo)5gM0o+7%1C&#?t3ILygqo+sU#5ca&ac8uCrUaS;7?^WQiWO~`m<}UW zQr>g~6h)YgCL0bH32)YiHw;bfLQaexGDB31Sa>Jwo$yeW{c{Hi6HQzt8#1vlU$lv9v;O3-C5A7U9gioy3sIDd^FTg(=JbC*M`z^q^`IhZAcKd;ol<=l;%K3h z^i8p~v>0Vu!>)yf%n5;lcKLCVI53~=8SU$@bE@-?z#~*X*g+Qxa|V|oQT$9HeaxF^ zi<)9>+aJk)#pJEz`Zo4%z2s`XyBN72;Ykqt>-9!QQTXd089DCx&9|+c-3P=UKM!#R zfM&7h|5YM)>L3-p>R(n+-_LC{QF+@Ez4+8_pa2KN2Si1OTvKQB5=!~@JgG4h^6B%vnpwfsEmkEF@>OB2o!4*F}V^x0gcL~v(1*uJxs24v+rgCnF2OE+-Y*IbZ1YgyCX#M;52J=}Q;O%nL z-SzN%$yIcJGoAY|nEU$Ito4yzv2NTNg`U$xj*p~fA5m2hF6i>5*3M!*q-6xy5> z{OJ_q{8cmQM9;#&_g>gQFz5#X%HQWl@wZMu#|kuw-`>%dl|28t5ewUfFC-e~lFIO%+jFkc<}Vg( z&)tzr3yx>N0ZgoNuFO~2LRsu_;YhChJYk~eX`q-^2rP`a{zBcI5?coIp|WiS(PSWZ zGkzlhV`YOxO1cTk$__t*|WyK=>x5 zz#AozePTN=C>`N#Q)A@nqr%0Yrxzbrznm;^ztQf6e;hVou@ z;yOxAYdz?nB`j|b>r2sbl>oy$Uatq*=p`cpJs-4Ow%Z6|FhdN**rX#~!Nwbg+mXk4 z>)oiB1{X^iy`W)gx$nx1irb4Ru1R$Oe-O2n$-0q|A(ju_8{sg2XF64}r|`z{V~W@} zOBT*zaJQR4NpYrg_fCS(V?J=c%8`PWQuTfF19RLHa0lvm{RjL3%!ZmA27(bw_Ur0c~?)+5P1+@5YOmu!&V81`Tt@Pxz z9R}!9tb`3X@QERP3F4#g647-3P)tQmV0rx`^{FjbS4q%im7z&&_zOTjv%+-?)J(F@ zk9m`VJ*LaToaLLWXHbhc?2tpn%*y30{BI0{&u7)Z`dRtrEQoZ@IT* zin-zP0uG9=Us`irHXUmq7#8^+KatxN^18TIHMKI(}?OS!jE ziSI?m_!(QoMtA_*Q4FV+rq^M@j1)sW$aA1Gz}$jCU^#TwFN;R?0M1rIGx#=nq_C5R z1&klS>b#HlSvAcIJ16hN!Ys+)%4bojI|I9;tlQabyd<3C9)l-=lqW7DRznW>QX*NB z^b3Uu(Of{2gC_FW%#5mV>63X$$9G9K!MsJVCDqN$dzXy0s*<#qKPU z_8}5vvhW|l^j2RB@Wj#shc-QSc$a!jT^ctD8&l(O*nN&iv+q@i17sx=L*nHVB%c56 z^F5k5(klUHFm$D_@ou7|w{$C;|6v_n_Q?30Cf{0cm))Gb@;q5FZl*qpU?MG&rZM_HR66M_wwm+ zwW~Wa{9y>-d=c0j9k{Sn^~|66XdFtt@NG9bHhe&VQKucNg*r0FFBQ0DLBLK=cL;S*$e-V#SIz!EMK*P!RKBgXkT)tFq44 zD#&Jti-D&gl-hESbWRxpb;9xkrU7dxGK8xtO4?wWC~vHAKyKH>C_E{0YFz3wcYprqU~4-A5~j! zkeyPLxAXwREaY)1DB+grPQuqBEB%USFdGp1GY^IJ^ix135|Uk5?e#)&S?SVb!6+7pZDNl$109it|*Y4EnP z{oPplK2xE4pPM7}pP*FJHLpZ9)&>0f4J;SszHNrJE9fc%icfs=y0lL?Iqm76P7hds zHxoX2pJ~{v9_*e;r5ZX+cImcuyS|SmTga?34;nFp3Ff-L=&>Anmpb>8%qQ%K*?94< z+S0Y2*(&a;->!-~^!J`r*t!@4ZZ5h<1HTL@niY&7M~X0WRy(IC{L6}T#4MH&<5W-))C>8wY15@A?MXe`0Tglz)%vL}GD2j)(vxLoMAB9~Ase(Nx(CzrsTt3#0dz@9Au+viEjRIWt z_Oj&@a%Ci4?c3n0&bHD@guq2LnVB*d7g*y=Z!3dhCz?{vjVEqW%rfw{-U+0J&i?xuLXj8Z>yl$;^%?nS1w6a-^3Ed+ zSVhW@e9(vQyoHUDhA+Bc>H{u()u_!?Sa}$VDwdAU?6-}tANYEPW~U!ixs=Upb)6Hl(59 zZX-@m>&}y8c^#=of_iwNuP>q#>(V6ZL+k0b&PM9LT(>R%2lH`(zBrgeAld0(6OE&L zWz@z^d_G$k#ES`*7lH*nVz(Ml5;@KA-eoztDGGdJZwoXA*o>s!j9rk!-p8uFOjXBM{Nft zAz=+bhhzqI?{byrR@nMv>cry!g{-GcM-D{n=@x=OKkp<~m5rGc;hfDPT(ou=#FX7Btt&n7 zp+t;E>nG@~v7s$}0)yEn5*<j%0cRuCO~4Fu@B~nKckW+Z-^JBh5?jc+I^gXz9osaLF)92G z1uXK;mPHgJil^)CefES#HmmZR?}Sc2lwmy z`IwO8e1-N*v4?0k@C#W)Ck1#eUly^G?*SS)Wit+}Ro^E$RoXXysyvqwBwy81#kx{- zhb$htu^)P(B!c7IM{8i_abc`bOy41fNq#_!+N*54Shrw3Osr+bg%gHy(JVi#Q80n?5{#%Pd(23K?iomso znz`@n>fwrllos(ar8jn++>lY%IQH`!x1({%;pR7-69!`w9-tLX@{2dtN-_{JUBFRK zUie(bC3|?Xw&Tb;fzl)$_Xwv>4C3T3`~M5{E#RkVwsW$M8jXLOxWPjII=A~HuHO6y z5ka+qCh2pzuh|+bIlbh!g9>z@>fzF4MsuZlU0FcitnNxiauZX8cl;{=HVj~FC%=S1 zDQ5o7CozkCQO5e}${LigkE%&}<;kY!Rdz5q2efoxjnjEw>~1NhqaR-}=cZt;;W*4ICWKi@G9Vqbu8G3LV02jg`of=K~p;>cjLt+P8 zE6vh!2=lKyBNHT8DyzL2+(?Pd$r67fOYd{Bo6SJ2am^DU!_i;UZA} zX`9k)@Q#(`1>@S7PVn+0`FtGYpjE8lptNjkQA9MtY>3Gge-+=v;TbJ-TVKc#h%@J5 zE)ek`U-jLym5?9}@Cp9AeM^N-C$erL@y!+Kb|?B~2HUDjsQvSs+KGJVgvZfXJn+7QtN;jE9!&{PfL7YVE*suG_@u_#KJ-vz%#Dxd=PA z6WYvmQYA?true%RwU#Sgx$p{gu@X4L{U;tJ`Os=@}2mjjd?q za$CPqfgML%)R2Vu|ECouY(V_o8&7?}@4qIqi25~|Ln#r!tW7p)6XhI751g-Y+UC4R z>T5oWwm|Lkw($^hA2Ht_&^PR8yL~J|GO{%0Ap%Fa!e0S(7Nnv{E&-uynSnvx6KvcH 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�-06LpEFytMyC$w|o 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�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
    G5>{m*S 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 From d25f3bd13e9836dabf1ab8a0042dc55d155e6850 Mon Sep 17 00:00:00 2001 From: Jitvar Patil Date: Fri, 16 Jan 2026 23:15:38 +0530 Subject: [PATCH 10/10] Remove .kiro from git tracking --- .kiro/specs/v5-calling-docs/design.md | 212 -------------------- .kiro/specs/v5-calling-docs/requirements.md | 90 --------- .kiro/specs/v5-calling-docs/tasks.md | 176 ---------------- 3 files changed, 478 deletions(-) delete mode 100644 .kiro/specs/v5-calling-docs/design.md delete mode 100644 .kiro/specs/v5-calling-docs/requirements.md delete mode 100644 .kiro/specs/v5-calling-docs/tasks.md diff --git a/.kiro/specs/v5-calling-docs/design.md b/.kiro/specs/v5-calling-docs/design.md deleted file mode 100644 index fa1c24d0..00000000 --- a/.kiro/specs/v5-calling-docs/design.md +++ /dev/null @@ -1,212 +0,0 @@ -# V5 Calls SDK Documentation - Design Document - -## Overview -Technical design for the V5 CometChat Calls SDK documentation structure based on SDK source code analysis. - -## SDK Architecture - -### Core Classes -1. **CometChatCalls** - Main SDK entry point (static methods) -2. **CallAppSettings** - SDK initialization configuration -3. **SessionSettings** - Call session configuration -4. **CallSession** - Active session management (singleton) -5. **Listeners** - Event handling interfaces - -### Key Methods (CometChatCalls) - -| Method | Description | -|--------|-------------| -| `init(context, callAppSettings, listener)` | Initialize SDK | -| `login(uid, apiKey, listener)` | Login with UID | -| `login(authToken, listener)` | Login with Auth Token | -| `logout(listener)` | Logout user | -| `getLoggedInUser()` | Get current user | -| `generateToken(sessionId, listener)` | Generate call token | -| `joinSession(sessionId, settings, container, listener)` | Join a session | - -### SessionSettings Options - -| Property | Type | Default | Description | -|----------|------|---------|-------------| -| `title` | String | null | Session title | -| `displayName` | String | null | User display name | -| `startVideoPaused` | Boolean | false | Start with video off | -| `startAudioMuted` | Boolean | false | Start with audio muted | -| `layout` | LayoutType | TILE | TILE, SPOTLIGHT | -| `type` | SessionType | VIDEO | VIDEO, AUDIO | -| `audioMode` | AudioMode | SPEAKER | SPEAKER, EARPIECE, BLUETOOTH | -| `initialCameraFacing` | CameraFacing | FRONT | FRONT, BACK | -| `idleTimeoutPeriod` | Int | 300 | Timeout in seconds | -| `lowBandwidthMode` | Boolean | false | Enable low bandwidth | -| `autoStartRecording` | Boolean | false | Auto-start recording | -| `hideControlPanel` | Boolean | false | Hide bottom controls | -| `hideHeaderPanel` | Boolean | false | Hide top header | -| `hideLeaveSessionButton` | Boolean | false | Hide leave button | -| `hideToggleAudioButton` | Boolean | false | Hide audio toggle | -| `hideToggleVideoButton` | Boolean | false | Hide video toggle | -| `hideSwitchCameraButton` | Boolean | false | Hide camera switch | -| `hideRecordingButton` | Boolean | true | Hide recording button | -| `hideScreenSharingButton` | Boolean | false | Hide screen share | -| `hideAudioModeButton` | Boolean | false | Hide audio mode | -| `hideRaiseHandButton` | Boolean | false | Hide raise hand | -| `hideShareInviteButton` | Boolean | true | Hide share invite | -| `hideParticipantListButton` | Boolean | false | Hide participant list | -| `hideChangeLayoutButton` | Boolean | false | Hide layout change | -| `hideChatButton` | Boolean | true | Hide chat button | -| `hideSessionTimer` | Boolean | false | Hide session timer | - -### CallSession Methods - -| Category | Method | Description | -|----------|--------|-------------| -| **Audio** | `muteAudio()` | Mute local audio | -| | `unMuteAudio()` | Unmute local audio | -| | `setAudioMode(AudioMode)` | Change audio output | -| **Video** | `pauseVideo()` | Pause local video | -| | `resumeVideo()` | Resume local video | -| | `switchCamera()` | Switch front/back camera | -| **Recording** | `startRecording()` | Start session recording | -| | `stopRecording()` | Stop session recording | -| **Participants** | `muteParticipant(id)` | Mute a participant | -| | `pauseParticipantVideo(id)` | Pause participant video | -| | `pinParticipant()` | Pin participant | -| | `unPinParticipant()` | Unpin participant | -| **Hand Raise** | `raiseHand()` | Raise hand | -| | `lowerHand()` | Lower hand | -| **Layout** | `setLayout(LayoutType)` | Change call layout | -| | `enablePictureInPictureLayout()` | Enable PiP | -| | `disablePictureInPictureLayout()` | Disable PiP | -| **Session** | `leaveSession()` | Leave the session | -| | `isSessionActive()` | Check session status | -| **UI** | `setChatButtonUnreadCount(count)` | Set chat badge | - -### Listeners - -#### SessionStatusListener -- `onSessionJoined()` -- `onSessionLeft()` -- `onSessionTimedOut()` -- `onConnectionLost()` -- `onConnectionRestored()` -- `onConnectionClosed()` - -#### ParticipantEventListener -- `onParticipantJoined(Participant)` -- `onParticipantLeft(Participant)` -- `onParticipantAudioMuted(Participant)` -- `onParticipantAudioUnmuted(Participant)` -- `onParticipantVideoPaused(Participant)` -- `onParticipantVideoResumed(Participant)` -- `onParticipantHandRaised(Participant)` -- `onParticipantHandLowered(Participant)` -- `onParticipantStartedScreenShare(Participant)` -- `onParticipantStoppedScreenShare(Participant)` -- `onParticipantStartedRecording(Participant)` -- `onParticipantStoppedRecording(Participant)` -- `onDominantSpeakerChanged(Participant)` -- `onParticipantListChanged(List)` - -#### MediaEventsListener -- `onRecordingStarted()` -- `onRecordingStopped()` -- `onScreenShareStarted()` -- `onScreenShareStopped()` -- `onAudioModeChanged(AudioMode)` -- `onCameraFacingChanged(CameraFacing)` -- `onAudioMuted()` -- `onAudioUnMuted()` -- `onVideoPaused()` -- `onVideoResumed()` - -#### ButtonClickListener -- `onLeaveSessionButtonClicked()` -- `onRaiseHandButtonClicked()` -- `onShareInviteButtonClicked()` -- `onChangeLayoutButtonClicked()` -- `onParticipantListButtonClicked()` -- `onToggleAudioButtonClicked()` -- `onToggleVideoButtonClicked()` -- `onSwitchCameraButtonClicked()` -- `onChatButtonClicked()` -- `onRecordingToggleButtonClicked()` - -#### LayoutListener -- `onCallLayoutChanged(LayoutType)` -- `onParticipantListVisible()` -- `onParticipantListHidden()` -- `onPictureInPictureLayoutEnabled()` -- `onPictureInPictureLayoutDisabled()` - -## File Structure - -``` -docs/sdk/android/calls/ -├── overview.mdx -├── setup.mdx -├── authentication.mdx -├── generate-token.mdx -├── session-settings.mdx -├── join-session.mdx -├── audio-controls.mdx -├── video-controls.mdx -├── recording.mdx -├── participant-actions.mdx -├── layout-ui.mdx -├── session-control.mdx -├── session-status-listener.mdx -├── participant-event-listener.mdx -├── media-events-listener.mdx -├── button-click-listener.mdx -└── layout-listener.mdx -``` - -## Navigation Structure (docs.json) - -```json -{ - "dropdown": "Android", - "icon": "/images/icons/android.svg", - "groups": [ - { - "group": "Overview", - "pages": ["sdk/android/calls/overview"] - }, - { - "group": "Getting Started", - "pages": [ - "sdk/android/calls/setup", - "sdk/android/calls/authentication" - ] - }, - { - "group": "Join Session", - "pages": [ - "sdk/android/calls/generate-token", - "sdk/android/calls/session-settings", - "sdk/android/calls/join-session" - ] - }, - { - "group": "Session Methods", - "pages": [ - "sdk/android/calls/audio-controls", - "sdk/android/calls/video-controls", - "sdk/android/calls/recording", - "sdk/android/calls/participant-actions", - "sdk/android/calls/layout-ui", - "sdk/android/calls/session-control" - ] - }, - { - "group": "Listeners", - "pages": [ - "sdk/android/calls/session-status-listener", - "sdk/android/calls/participant-event-listener", - "sdk/android/calls/media-events-listener", - "sdk/android/calls/button-click-listener", - "sdk/android/calls/layout-listener" - ] - } - ] -} -``` diff --git a/.kiro/specs/v5-calling-docs/requirements.md b/.kiro/specs/v5-calling-docs/requirements.md deleted file mode 100644 index 536ef751..00000000 --- a/.kiro/specs/v5-calling-docs/requirements.md +++ /dev/null @@ -1,90 +0,0 @@ -# V5 Calls SDK Documentation - -## Overview -Create comprehensive documentation for the V5 CometChat Calls SDK covering setup, authentication, session management, methods, and listeners. - -## User Stories - -### Story 1: SDK Overview & Setup -**As a** developer new to CometChat Calls SDK -**I want** clear setup instructions and SDK overview -**So that** I can quickly integrate calling into my application - -#### Acceptance Criteria -- [ ] Overview page explains SDK capabilities and architecture -- [ ] Setup page covers installation, dependencies, and permissions -- [ ] CallAppSettings initialization is documented with examples -- [ ] Authentication methods (login with UID, authToken, logout) are documented - -### Story 2: Join Session Documentation -**As a** developer implementing calling features -**I want** detailed documentation on joining call sessions -**So that** I can properly configure and start call sessions - -#### Acceptance Criteria -- [ ] Generate Token method is documented -- [ ] SessionSettings builder with all configuration options is documented -- [ ] joinSession method with examples is documented -- [ ] CallSession object and its lifecycle is explained - -### Story 3: Session Methods Documentation -**As a** developer controlling an active call session -**I want** documentation for all CallSession methods -**So that** I can implement audio/video controls, recording, and participant management - -#### Acceptance Criteria -- [ ] Audio controls (mute, unmute, setAudioMode) are documented -- [ ] Video controls (pause, resume, switchCamera) are documented -- [ ] Recording methods (start, stop) are documented -- [ ] Participant actions (mute, pause video, pin) are documented -- [ ] Layout and UI methods are documented -- [ ] Session control methods (leave, raiseHand) are documented - -### Story 4: Listeners Documentation -**As a** developer handling call events -**I want** documentation for all listener interfaces -**So that** I can respond to session, participant, and media events - -#### Acceptance Criteria -- [ ] SessionStatusListener with all callbacks is documented -- [ ] ParticipantEventListener with all callbacks is documented -- [ ] MediaEventsListener with all callbacks is documented -- [ ] ButtonClickListener with all callbacks is documented -- [ ] LayoutListener with all callbacks is documented - -### Story 5: External Resources -**As a** developer exploring the SDK -**I want** links to sample apps and changelogs -**So that** I can see working examples and track SDK updates - -#### Acceptance Criteria -- [ ] Sample App link is included for each platform -- [ ] GitHub Changelog link is included for each platform - -## Documentation Structure (Per Platform) -``` -Calling SDK -├── Overview -├── Getting Started -│ ├── Setup -│ └── Authentication -├── Join Session -│ ├── Generate Token -│ ├── Session Settings -│ └── Join Session -├── Session Methods -│ ├── Audio Controls -│ ├── Video Controls -│ ├── Recording -│ ├── Participant Actions -│ ├── Layout & UI -│ └── Session Control -├── Listeners -│ ├── Session Status Listener -│ ├── Participant Event Listener -│ ├── Media Events Listener -│ ├── Button Click Listener -│ └── Layout Listener -├── Sample App (link) -└── Changelog (link) -``` diff --git a/.kiro/specs/v5-calling-docs/tasks.md b/.kiro/specs/v5-calling-docs/tasks.md deleted file mode 100644 index fda93015..00000000 --- a/.kiro/specs/v5-calling-docs/tasks.md +++ /dev/null @@ -1,176 +0,0 @@ -# V5 Calls SDK Documentation - Implementation Tasks - -## Overview -Implementation plan for creating V5 Calls SDK documentation for all platforms, using Android as the reference implementation. - -## Tasks - -- [x] 1. Create documentation file structure - - Created `docs/calls/android/` directory with all documentation files - - _Requirements: Story 1_ - -- [x] 2. Create Overview documentation - - [x] 2.1 Create `overview.mdx` with SDK introduction, features, architecture, prerequisites - - _Requirements: Story 1_ - -- [x] 3. Create Getting Started documentation - - [x] 3.1 Create `setup.mdx` with installation, permissions, CallAppSettings initialization - - [x] 3.2 Create `authentication.mdx` with login (UID, authToken), logout, getLoggedInUser - - _Requirements: Story 1_ - -- [x] 4. Create Join Session documentation - - [x] 4.1 Create `join-session.mdx` with generateToken(), SessionSettings, joinSession() and CallSession lifecycle (consolidated) - - _Requirements: Story 2_ - -- [x] 5. Create Session Methods documentation - - [x] 5.1 Create `audio-controls.mdx` with muteAudio, unMuteAudio, setAudioMode - - [x] 5.2 Create `video-controls.mdx` with pauseVideo, resumeVideo, switchCamera - - [x] 5.3 Create `recording.mdx` with startRecording, stopRecording - - [x] 5.4 Create `participant-actions.mdx` with muteParticipant, pauseParticipantVideo, pin/unpin - - [x] 5.5 Create `layout-ui.mdx` with setLayout, PiP methods, setChatButtonUnreadCount - - [x] 5.6 Create `session-control.mdx` with leaveSession, raiseHand, lowerHand, isSessionActive - - _Requirements: Story 3_ - -- [x] 6. Create Listeners documentation - - [x] 6.1 Create `session-status-listener.mdx` with all SessionStatusListener callbacks - - [x] 6.2 Create `participant-event-listener.mdx` with all ParticipantEventListener callbacks - - [x] 6.3 Create `media-events-listener.mdx` with all MediaEventsListener callbacks - - [x] 6.4 Create `button-click-listener.mdx` with all ButtonClickListener callbacks - - [x] 6.5 Create `layout-listener.mdx` with all LayoutListener callbacks - - _Requirements: Story 4_ - -- [ ] 7. Update docs.json navigation - - [ ] 7.1 Add Calls SDK product section with Android dropdown - - [ ] 7.2 Add navigation groups: Overview, Getting Started, Join Session, Session Methods, Listeners - - [ ] 7.3 Add Sample App and Changelog external links for Android - - _Requirements: Story 5_ - -- [ ] 8. Checkpoint - Review Android documentation - - Ensure all pages are accessible and properly linked - - Verify code examples compile correctly - -- [ ] 9. Create iOS documentation - - [ ] 9.1 Create `docs/calls/ios/setup.mdx` with CocoaPods/SPM installation, permissions, initialization - - [ ] 9.2 Create `docs/calls/ios/authentication.mdx` with Swift login/logout examples - - [ ] 9.3 Create `docs/calls/ios/join-session.mdx` with generateToken, SessionSettings, joinSession - - [ ] 9.4 Create `docs/calls/ios/audio-controls.mdx` with Swift audio control examples - - [ ] 9.5 Create `docs/calls/ios/video-controls.mdx` with Swift video control examples - - [ ] 9.6 Create `docs/calls/ios/recording.mdx` with Swift recording examples - - [ ] 9.7 Create `docs/calls/ios/participant-actions.mdx` with Swift participant management - - [ ] 9.8 Create `docs/calls/ios/layout-ui.mdx` with Swift layout/UI examples - - [ ] 9.9 Create `docs/calls/ios/session-control.mdx` with Swift session control examples - - [ ] 9.10 Create listener documentation files (session-status, participant-event, media-events, button-click, layout) - - [ ] 9.11 Update `docs/calls/ios/overview.mdx` with full iOS SDK overview - - [ ] 9.12 Add iOS navigation to docs.json - - _Requirements: Stories 1-5_ - -- [ ] 10. Create React Native documentation - - [ ] 10.1 Create `docs/calls/react-native/setup.mdx` with npm/yarn installation, native setup - - [ ] 10.2 Create `docs/calls/react-native/authentication.mdx` with TypeScript login/logout examples - - [ ] 10.3 Create `docs/calls/react-native/join-session.mdx` with generateToken, SessionSettings, joinSession - - [ ] 10.4 Create `docs/calls/react-native/audio-controls.mdx` with TypeScript audio control examples - - [ ] 10.5 Create `docs/calls/react-native/video-controls.mdx` with TypeScript video control examples - - [ ] 10.6 Create `docs/calls/react-native/recording.mdx` with TypeScript recording examples - - [ ] 10.7 Create `docs/calls/react-native/participant-actions.mdx` with TypeScript participant management - - [ ] 10.8 Create `docs/calls/react-native/layout-ui.mdx` with TypeScript layout/UI examples - - [ ] 10.9 Create `docs/calls/react-native/session-control.mdx` with TypeScript session control examples - - [ ] 10.10 Create listener documentation files - - [ ] 10.11 Update `docs/calls/react-native/overview.mdx` with full React Native SDK overview - - [ ] 10.12 Add React Native navigation to docs.json - - _Requirements: Stories 1-5_ - -- [ ] 11. Create JavaScript documentation - - [ ] 11.1 Create `docs/calls/javascript/setup.mdx` with npm/CDN installation, initialization - - [ ] 11.2 Create `docs/calls/javascript/authentication.mdx` with JavaScript login/logout examples - - [ ] 11.3 Create `docs/calls/javascript/join-session.mdx` with generateToken, SessionSettings, joinSession - - [ ] 11.4 Create `docs/calls/javascript/audio-controls.mdx` with JavaScript audio control examples - - [ ] 11.5 Create `docs/calls/javascript/video-controls.mdx` with JavaScript video control examples - - [ ] 11.6 Create `docs/calls/javascript/recording.mdx` with JavaScript recording examples - - [ ] 11.7 Create `docs/calls/javascript/participant-actions.mdx` with JavaScript participant management - - [ ] 11.8 Create `docs/calls/javascript/layout-ui.mdx` with JavaScript layout/UI examples - - [ ] 11.9 Create `docs/calls/javascript/session-control.mdx` with JavaScript session control examples - - [ ] 11.10 Create listener documentation files - - [ ] 11.11 Update `docs/calls/javascript/overview.mdx` with full JavaScript SDK overview - - [ ] 11.12 Add JavaScript navigation to docs.json - - _Requirements: Stories 1-5_ - -- [ ] 12. Create Flutter documentation - - [ ] 12.1 Create `docs/calls/flutter/setup.mdx` with pub.dev installation, native setup - - [ ] 12.2 Create `docs/calls/flutter/authentication.mdx` with Dart login/logout examples - - [ ] 12.3 Create `docs/calls/flutter/join-session.mdx` with generateToken, SessionSettings, joinSession - - [ ] 12.4 Create `docs/calls/flutter/audio-controls.mdx` with Dart audio control examples - - [ ] 12.5 Create `docs/calls/flutter/video-controls.mdx` with Dart video control examples - - [ ] 12.6 Create `docs/calls/flutter/recording.mdx` with Dart recording examples - - [ ] 12.7 Create `docs/calls/flutter/participant-actions.mdx` with Dart participant management - - [ ] 12.8 Create `docs/calls/flutter/layout-ui.mdx` with Dart layout/UI examples - - [ ] 12.9 Create `docs/calls/flutter/session-control.mdx` with Dart session control examples - - [ ] 12.10 Create listener documentation files - - [ ] 12.11 Update `docs/calls/flutter/overview.mdx` with full Flutter SDK overview - - [ ] 12.12 Add Flutter navigation to docs.json - - _Requirements: Stories 1-5_ - -- [ ] 13. Create Ionic documentation - - [ ] 13.1 Create `docs/calls/ionic/setup.mdx` with npm installation, Capacitor/Cordova setup - - [ ] 13.2 Create `docs/calls/ionic/authentication.mdx` with TypeScript login/logout examples - - [ ] 13.3 Create `docs/calls/ionic/join-session.mdx` with generateToken, SessionSettings, joinSession - - [ ] 13.4 Create `docs/calls/ionic/audio-controls.mdx` with TypeScript audio control examples - - [ ] 13.5 Create `docs/calls/ionic/video-controls.mdx` with TypeScript video control examples - - [ ] 13.6 Create `docs/calls/ionic/recording.mdx` with TypeScript recording examples - - [ ] 13.7 Create `docs/calls/ionic/participant-actions.mdx` with TypeScript participant management - - [ ] 13.8 Create `docs/calls/ionic/layout-ui.mdx` with TypeScript layout/UI examples - - [ ] 13.9 Create `docs/calls/ionic/session-control.mdx` with TypeScript session control examples - - [ ] 13.10 Create listener documentation files - - [ ] 13.11 Update `docs/calls/ionic/overview.mdx` with full Ionic SDK overview - - [ ] 13.12 Add Ionic navigation to docs.json - - _Requirements: Stories 1-5_ - -- [ ] 14. Final checkpoint - - Verify all platform documentation is complete - - Test navigation across all platforms - - Ensure consistent structure and terminology - -## Current File Structure - -``` -docs/calls/ -├── android/ -│ ├── overview.mdx ✓ -│ ├── setup.mdx ✓ -│ ├── authentication.mdx ✓ -│ ├── join-session.mdx ✓ (includes generate-token & session-settings) -│ ├── audio-controls.mdx ✓ -│ ├── video-controls.mdx ✓ -│ ├── recording.mdx ✓ -│ ├── participant-actions.mdx ✓ -│ ├── layout-ui.mdx ✓ -│ ├── session-control.mdx ✓ -│ ├── session-status-listener.mdx ✓ -│ ├── participant-event-listener.mdx ✓ -│ ├── media-events-listener.mdx ✓ -│ ├── button-click-listener.mdx ✓ -│ ├── layout-listener.mdx ✓ -│ ├── listeners.mdx ✓ -│ └── session-methods.mdx ✓ -├── ios/ -│ └── overview.mdx (placeholder) -├── react-native/ -│ └── overview.mdx (placeholder) -├── javascript/ -│ └── overview.mdx (placeholder) -├── flutter/ -│ └── overview.mdx (placeholder) -├── ionic/ -│ └── overview.mdx (placeholder) -└── api/ - ├── overview.mdx ✓ - ├── get-call.mdx ✓ - └── list-calls.mdx ✓ -``` - -## Notes -- Android documentation is complete and serves as the reference implementation -- docs.json navigation needs to be updated to include the Calls SDK section -- Other platforms (iOS, React Native, JavaScript, Flutter, Ionic) need full documentation -- Use SDK source code for accurate method signatures and parameters -- Include platform-appropriate code examples (Kotlin/Java for Android, Swift for iOS, etc.) -- Ensure consistent structure across all platforms

    Ah1bT1xh*zpZ*JpN~Mzj62?p5 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 M07*qoM6N<$g7&x*BLDyZ literal 469591 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Ɯ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