diff --git a/.styles/config/vocabularies/Base/accept.txt b/.styles/config/vocabularies/Base/accept.txt index 27787c968..dc256a1d3 100644 --- a/.styles/config/vocabularies/Base/accept.txt +++ b/.styles/config/vocabularies/Base/accept.txt @@ -35,4 +35,5 @@ stream_video stream_video_push_notification screensharing Livestreaming -livestreaming \ No newline at end of file +livestreaming +callee \ No newline at end of file diff --git a/docusaurus/docs/Flutter/05-advanced/02-ringing.mdx b/docusaurus/docs/Flutter/05-advanced/02-ringing.mdx index 2ecedf1ad..85eb1ddf4 100644 --- a/docusaurus/docs/Flutter/05-advanced/02-ringing.mdx +++ b/docusaurus/docs/Flutter/05-advanced/02-ringing.mdx @@ -24,6 +24,176 @@ To receive push notifications from Stream Video, you'll need to: To get the best experience we strongly suggest using APNs for iOS. While our goal is to ensure compatibility with both providers on iOS, Firebase is not yet fully supported. ::: +### Creating a ringing Call + +To create a ringing call, you need to follow the same steps as in the basic call flow with the difference of adding the `ringing` and `memberIds` parameters to the `getOrCreateCall()` method. + +```dart +final call = StreamVideo.instance.makeCall(callType: StreamCallType(), id: 'Your-call-ID'); +await call.getOrCreate(memberIds: ['user1_id', 'user2_id'], ringing: true); +``` + +* Setting `ringing` to true will make Stream send a notification to the users on the call, triggering the platform call screen on iOS and Android. +* `memberIds` is a list of user IDs to immediately add to the call. Combining this with a `ringing` parameter will trigger the call to ring on the devices of the members. + +In subsequent steps, we will show you how to configure your Stream app in the Dashboard and your Flutter app to properly send and receive ringing notifications. + +### Common steps for both iOS and Android + +#### Configuring Push Notification Manager + +To handle push notifications, you need to configure the `pushNotificationManagerProvider` in the `StreamVideo` instance. +It manages device token registration, incoming call handling, and listening to call events (for example to end the call on the callee side when the caller ends the call). + +When creating a `StreamVideo` instance, you need to pass a `pushNotificationManagerProvider` parameter. This parameter is an instance of `StreamVideoPushNotificationManager` that is created using the `StreamVideoPushNotificationManager.create` method. + +```dart + StreamVideo( + // ... + // Make sure you initialise push notification manager + pushNotificationManagerProvider: StreamVideoPushNotificationManager.create( + iosPushProvider: const StreamVideoPushProvider.apn( + name: 'your-ios-provider-name', + ), + androidPushProvider: const StreamVideoPushProvider.firebase( + name: 'your-fcm-provider', + ), + pushParams: const StreamVideoPushParams( + appName: kAppName, + ios: IOSParams(iconName: 'IconMask'), + ), + ), + ); +``` + +- For `androidPushProvider` use the provider name we will create later in [Firebase integration](#Step-2---Upload-the-Firebase-Credentials-to-Stream) + +- For `iosPushProvider` use the provider name we will create later in [APN integration](#Integrating-APNs-for-iOS​) + +- Add app icon Asset in Xcode for displaying in CallKit screen dedicated button (named `IconMask` in the code below). [See details here](https://developer.apple.com/documentation/callkit/cxproviderconfiguration/2274376-icontemplateimagedata) + +#### Handling CallKit events (for both iOS and Android) + +CallKit events are events exposed by the `flutter_callkit_incoming` package that we utilize to handle incoming calls on both iOS and Android. +It is important to handle these events to ensure a seamless calling experience regardless of which provider is used for push. + +In a high-level widget in your app, add this code to listen to CallKit events: + +```dart +final _callKitEventSubscriptions = Subscriptions(); + +@override +void initState() { + ... + _observeCallKitEvents() +} + +void _observeCallKitEvents() { + final streamVideo = StreamVideo.instance; + _callKitEventSubscriptions.addAll([ + streamVideo.onCallKitEvent(_onCallAccept), + streamVideo.onCallKitEvent(_onCallDecline), + streamVideo.onCallKitEvent(_onCallEnded), + // You can add additional events if you need to handle them + // As an example, handling mute events from CallKit + ]); +} + +void _onCallAccept(ActionCallAccept event) async { + final streamVideo = StreamVideo.instance; + + final uuid = event.data.uuid; + final cid = event.data.callCid; + if (uuid == null || cid == null) return; + + final call = await streamVideo.consumeIncomingCall(uuid: uuid, cid: cid); + final callToJoin = call.getDataOrNull(); + if (callToJoin == null) return; + + var acceptResult = await callToJoin.accept(); + + // Return if cannot accept call + if(acceptResult.isFailure) { + debugPrint('Error accepting call: $call'); + return; + } + + // Data for next screen + final extra = ( + call: callToJoin, + connectOptions: const CallConnectOptions(), + ); + + // Navigate to next screen if needed +} + +void _onCallDecline(ActionCallDecline event) async { + final streamVideo = StreamVideo.instance; + + final uuid = event.data.uuid; + final cid = event.data.callCid; + if (uuid == null || cid == null) return; + + final call = await streamVideo.consumeIncomingCall(uuid: uuid, cid: cid); + final callToReject = call.getDataOrNull(); + if (callToReject == null) return; + + final result = await callToReject.reject(); + if (result is Failure) { + debugPrint('Error rejecting call: ${result.error}'); + } +} + +void _onCallEnded(ActionCallEnded event) async { + final streamVideo = StreamVideo.instance; + + final uuid = event.data.uuid; + final cid = event.data.callCid; + if (uuid == null || cid == null) return; + + final call = streamVideo.activeCall; + if (call == null || call.callCid.value != cid) return; + + final result = await call.leave(); + if (result is Failure) { + debugPrint('Error leaving call: ${result.error}'); + } +} + +@override +void dispose() { + // ... + _callKitEventSubscriptions.cancelAll(); +} +``` + +:::note +Remember to implement navigation in a marked line. +::: + +Please add an additional extension at the end of the class / in a different file that we added to make +subscriptions simpler: + +```dart +extension on Subscriptions { + void addAll(Iterable?> subscriptions) { + for (var i = 0; i < subscriptions.length; i++) { + final subscription = subscriptions.elementAt(i); + if (subscription == null) continue; + + add(i + 100, subscription); + } + } +} +``` + +If you need to manage the CallKit call, you can use the `StreamVideo.pushNotificationManager`. As an example, let's +say you want to end all calls on the CallKit side, you can end them this way: + +```dart +StreamVideo.instance.pushNotificationManager?.endAllCalls(); +``` + ### Integrating Firebase for Android #### Step 1 - Get the Firebase Credentials @@ -97,11 +267,7 @@ a push notification is received. - Replace `yourUserCredentialsGetMethod()` with your implementation to get logged in user credentials -- For `androidPushProvider` use the provider name created in [Step 2](#Step-2---Upload-the-Firebase-Credentials-to-Stream) - -- For `iosPushProvider` use the provider name we will create later in [APN integration](#Integrating-APNs-for-iOS​) - -- Add app icon Asset in Xcode for displaying in CallKit screen dedicated button (named `IconMask` in the code below). [See details here](https://developer.apple.com/documentation/callkit/cxproviderconfiguration/2274376-icontemplateimagedata) +- Configure `pushNotificationManagerProvider` in the same way you did in the previous setup steps ```dart // As this runs in a separate isolate, we need to setup the app again. @@ -219,6 +385,7 @@ For Android 13+ you need to request the `POST_NOTIFICATIONS` permission. You can Remember to follow [official best practices](https://developer.android.com/develop/ui/views/notifications/notification-permission#best-practices) (especially showing prompt before the request). + ### Integrating APNs for iOS #### Step 1 - Get the iOS certificate for push notifications @@ -282,126 +449,7 @@ Add these permissions to `Info.plist` in order to support video calling: ``` -#### Step 5 - Add code to handle CallKit events - -In a high-level widget in your app, add this code to listen to CallKit events: - -```dart -final _callKitEventSubscriptions = Subscriptions(); - -@override -void initState() { - ... - _observeCallKitEvents() -} - -void _observeCallKitEvents() { - final streamVideo = StreamVideo.instance; - _callKitEventSubscriptions.addAll([ - streamVideo.onCallKitEvent(_onCallAccept), - streamVideo.onCallKitEvent(_onCallDecline), - streamVideo.onCallKitEvent(_onCallEnded), - // You can add additional events if you need to handle them - // As an example, handling mute events from CallKit - ]); -} - -void _onCallAccept(ActionCallAccept event) async { - final streamVideo = StreamVideo.instance; - - final uuid = event.data.uuid; - final cid = event.data.callCid; - if (uuid == null || cid == null) return; - - final call = await streamVideo.consumeIncomingCall(uuid: uuid, cid: cid); - final callToJoin = call.getDataOrNull(); - if (callToJoin == null) return; - - var acceptResult = await callToJoin.accept(); - - // Return if cannot accept call - if(acceptResult.isFailure) { - debugPrint('Error accepting call: $call'); - return; - } - - // Data for next screen - final extra = ( - call: callToJoin, - connectOptions: const CallConnectOptions(), - ); - - // Navigate to next screen if needed -} - -void _onCallDecline(ActionCallDecline event) async { - final streamVideo = StreamVideo.instance; - - final uuid = event.data.uuid; - final cid = event.data.callCid; - if (uuid == null || cid == null) return; - - final call = await streamVideo.consumeIncomingCall(uuid: uuid, cid: cid); - final callToReject = call.getDataOrNull(); - if (callToReject == null) return; - - final result = await callToReject.reject(); - if (result is Failure) { - debugPrint('Error rejecting call: ${result.error}'); - } -} - -void _onCallEnded(ActionCallEnded event) async { - final streamVideo = StreamVideo.instance; - - final uuid = event.data.uuid; - final cid = event.data.callCid; - if (uuid == null || cid == null) return; - - final call = streamVideo.activeCall; - if (call == null || call.callCid.value != cid) return; - - final result = await call.leave(); - if (result is Failure) { - debugPrint('Error leaving call: ${result.error}'); - } -} - -@override -void dispose() { - // ... - _callKitEventSubscriptions.cancelAll(); -} -``` - -:::note -Remember to implement navigation in a marked line. -::: - -Please add an additional extension at the end of the class / in a different file that we added to make -subscriptions simpler: - -```dart -extension on Subscriptions { - void addAll(Iterable?> subscriptions) { - for (var i = 0; i < subscriptions.length; i++) { - final subscription = subscriptions.elementAt(i); - if (subscription == null) continue; - - add(i + 100, subscription); - } - } -} -``` - -If you need to manage the CallKit call, you can use the `StreamVideo.pushNotificationManager`. As an example, let's -say you want to end all calls on the CallKit side, you can end them this way: - -```dart -StreamVideo.instance.pushNotificationManager?.endAllCalls(); -``` - -#### Step 6 - Add callback to handle call in terminated state +#### Step 5 - Add callback to handle call in terminated state When an iOS app is terminated, the Flutter engine is not running. The engine needs to be started up to handle Stream call events whenever a call is received by the app. The Stream SDK performs the job of running a Flutter engine instance whenever a call is received. However, on the app side, a callback handle needs to be registered that will connect to `StreamVideo`. @@ -443,7 +491,7 @@ The `_backgroundVoipCallHandler` method should then be set when StreamVideo is i ); ``` -#### Step 7 - Add native code to the iOS project +#### Step 6 - Add native code to the iOS project In your iOS project, add the following imports to your `AppDelegate.swift`: