Skip to content

Commit 1056c7e

Browse files
authored
chore(repo): make ringing work when example app is in the background (#1105)
* make ringing work when example app is in the background * readme * tweaks
1 parent d4d2d70 commit 1056c7e

File tree

12 files changed

+111
-78
lines changed

12 files changed

+111
-78
lines changed

packages/stream_video/test/src/call/call_coordinator_events_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1742,7 +1742,7 @@ void main() {
17421742

17431743
final initialState = createActiveCallState(
17441744
currentByUser: currentUser,
1745-
status: CallStatus.outgoing(acceptedByCallee: false),
1745+
status: CallStatus.outgoing(),
17461746
);
17471747
final stateManager = CallStateNotifier(initialState);
17481748

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,27 @@
11
# Stream Video Flutter Example App
22

3-
This is an example app built using the [Stream Video Flutter](https://pub.dev/packages/stream_video_flutter) package, designed to showcase both core features of the package. It's a great starting point to explore how the Stream Video SDK works in real-world scenarios.
3+
This example app showcases core features of the [Stream Video Flutter](https://pub.dev/packages/stream_video_flutter) package. It's a great starting point to see how the Stream Video SDK works in real-world scenarios.
44

55
For a more advanced example of how to use the Stream Video Flutter package, check out the [dogfooding app here](https://github.com/GetStream/stream-video-flutter/tree/main/dogfooding).
66

77
## Features
88

9-
- **Video Calls**: Test various aspects of video call functionality.
10-
- Create or join a video meeting (with or without a Lobby).
11-
- Make direct calls to other users (without CallKit integration - only in-app ringing screen)
9+
- **Video calls**: Explore core calling functionality.
10+
- Create or join a meeting (with or without a lobby).
11+
- Make direct calls to other users (with CallKit/ringing integration; calls work in the background).
12+
13+
> Note: In this example, ringing does not work when the app is terminated. For a more robust implementation of ringing with background capabilities, see the [dogfooding app](https://github.com/GetStream/stream-video-flutter/tree/main/dogfooding).
14+
15+
## Prerequisites
16+
17+
- Flutter installed and available on your PATH
18+
- A connected device or simulator/emulator
1219

1320
## Getting Started
1421

1522
To set up and run the app, follow these steps:
1623

17-
1. Clone the repo and install the dependencies
24+
1. Clone the repository and install dependencies
1825
First, clone the repository to your local machine and install the required Flutter packages:
1926

2027
```bash
@@ -23,11 +30,10 @@ cd stream-video-flutter/packages/stream_video_flutter/example
2330
flutter pub get
2431
```
2532

26-
2. Run the app
33+
2. Run the example
2734
Once the setup is complete, you can run the app using Flutter:
2835

2936
```bash
30-
## Ensure you're in the example directory
3137
flutter run
3238
```
3339

@@ -42,11 +48,17 @@ Licensed under the Stream License;
4248
you may not use this file except in compliance with the License.
4349
You may obtain a copy of the License at
4450
45-
https://github.com/GetStream/stream-video-android/blob/main/LICENSE
51+
https://github.com/GetStream/stream-video-flutter/blob/main/LICENSE
4652
4753
Unless required by applicable law or agreed to in writing, software
4854
distributed under the License is distributed on an "AS IS" BASIS,
4955
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
5056
See the License for the specific language governing permissions and
5157
limitations under the License.
52-
```
58+
```
59+
60+
## Links
61+
62+
- Package on pub.dev: https://pub.dev/packages/stream_video_flutter
63+
- Docs: https://getstream.io/video/docs/flutter/
64+
- Advanced example (dogfooding app): https://github.com/GetStream/stream-video-flutter/tree/main/dogfooding

packages/stream_video_flutter/example/ios/Flutter/AppFrameworkInfo.plist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,6 @@
2121
<key>CFBundleVersion</key>
2222
<string>1.0</string>
2323
<key>MinimumOSVersion</key>
24-
<string>12.0</string>
24+
<string>15.0</string>
2525
</dict>
2626
</plist>

packages/stream_video_flutter/example/ios/Podfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Uncomment this line to define a global platform for your project
2-
platform :ios, '14'
2+
platform :ios, '15'
33

44
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
55
ENV['COCOAPODS_DISABLE_STATS'] = 'true'

packages/stream_video_flutter/example/ios/Runner.xcodeproj/project.pbxproj

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
3535
2D45EC435B9C349C8F075378 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
3636
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
37+
60368AB02EC74DD600B3618A /* RunnerDebug.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerDebug.entitlements; sourceTree = "<group>"; };
3738
63C4214F6AF6EDCA0297B4E0 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
3839
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
3940
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@@ -112,6 +113,7 @@
112113
97C146F01CF9000F007C117D /* Runner */ = {
113114
isa = PBXGroup;
114115
children = (
116+
60368AB02EC74DD600B3618A /* RunnerDebug.entitlements */,
115117
97C146FA1CF9000F007C117D /* Main.storyboard */,
116118
97C146FD1CF9000F007C117D /* Assets.xcassets */,
117119
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
@@ -383,7 +385,7 @@
383385
"$(inherited)",
384386
"@executable_path/Frameworks",
385387
);
386-
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
388+
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.video.flutter.dogfooding;
387389
PRODUCT_NAME = "$(TARGET_NAME)";
388390
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
389391
SWIFT_VERSION = 5.0;
@@ -504,6 +506,7 @@
504506
buildSettings = {
505507
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
506508
CLANG_ENABLE_MODULES = YES;
509+
CODE_SIGN_ENTITLEMENTS = Runner/RunnerDebug.entitlements;
507510
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
508511
DEVELOPMENT_TEAM = EHV7XZLAHA;
509512
ENABLE_BITCODE = NO;
@@ -512,7 +515,7 @@
512515
"$(inherited)",
513516
"@executable_path/Frameworks",
514517
);
515-
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
518+
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.video.flutter.dogfooding;
516519
PRODUCT_NAME = "$(TARGET_NAME)";
517520
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
518521
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -535,7 +538,7 @@
535538
"$(inherited)",
536539
"@executable_path/Frameworks",
537540
);
538-
PRODUCT_BUNDLE_IDENTIFIER = com.example.example;
541+
PRODUCT_BUNDLE_IDENTIFIER = io.getstream.video.flutter.dogfooding;
539542
PRODUCT_NAME = "$(TARGET_NAME)";
540543
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
541544
SWIFT_VERSION = 5.0;

packages/stream_video_flutter/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
buildConfiguration = "Debug"
2727
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
2828
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
29+
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
2930
shouldUseLaunchSchemeArgsEnv = "YES">
3031
<MacroExpansion>
3132
<BuildableReference
@@ -43,11 +44,13 @@
4344
buildConfiguration = "Debug"
4445
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
4546
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
47+
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
4648
launchStyle = "0"
4749
useCustomWorkingDirectory = "NO"
4850
ignoresPersistentStateOnLaunch = "NO"
4951
debugDocumentVersioning = "YES"
5052
debugServiceExtension = "internal"
53+
enableGPUValidationMode = "1"
5154
allowLocationSimulation = "YES">
5255
<BuildableProductRunnable
5356
runnableDebuggingMode = "0">
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import UIKit
21
import Flutter
2+
import UIKit
3+
import stream_video_push_notification
34

45
@main
56
@objc class AppDelegate: FlutterAppDelegate {
@@ -8,6 +9,7 @@ import Flutter
89
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
910
) -> Bool {
1011
GeneratedPluginRegistrant.register(with: self)
12+
StreamVideoPKDelegateManager.shared.registerForPushNotifications()
1113
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
1214
}
1315
}

packages/stream_video_flutter/example/ios/Runner/Info.plist

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
33
<plist version="1.0">
44
<dict>
5+
<key>BGTaskSchedulerPermittedIdentifiers</key>
6+
<array>
7+
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
8+
</array>
59
<key>CADisableMinimumFrameDurationOnPhone</key>
610
<true/>
711
<key>CFBundleDevelopmentRegion</key>
@@ -35,6 +39,10 @@
3539
<key>UIBackgroundModes</key>
3640
<array>
3741
<string>audio</string>
42+
<string>processing</string>
43+
<string>remote-notification</string>
44+
<string>voip</string>
45+
<string>fetch</string>
3846
</array>
3947
<key>UILaunchStoryboardName</key>
4048
<string>LaunchScreen</string>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
3+
<plist version="1.0">
4+
<dict>
5+
<key>aps-environment</key>
6+
<string>development</string>
7+
</dict>
8+
</plist>

packages/stream_video_flutter/example/lib/app.dart

Lines changed: 57 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'package:flutter/material.dart';
99
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
1010
as ln;
1111
import 'package:google_fonts/google_fonts.dart';
12+
import 'package:rxdart/rxdart.dart';
1213
import 'package:stream_video_flutter/stream_video_flutter.dart';
1314

1415
import 'core/auth_repository.dart';
@@ -22,11 +23,9 @@ Future<void> _onFirebaseBackgroundMessage(RemoteMessage message) async {
2223
debugPrint(
2324
'[onFirebaseBackgroundMessage] message: ${jsonEncode(message.toMap())}',
2425
);
26+
2527
// Initialise Firebase
2628
await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform);
27-
await initNotifications();
28-
29-
// Initialise Stream Video
3029

3130
final authRepository = await AuthRepository.getInstance();
3231
final credentials = authRepository.getCredentials();
@@ -38,51 +37,15 @@ Future<void> _onFirebaseBackgroundMessage(RemoteMessage message) async {
3837
userToken: credentials.token,
3938
);
4039

41-
client.observeCallDeclinedRingingEvent();
42-
43-
await _handlePushNotification(message);
44-
45-
await StreamVideo.reset(disconnect: true);
46-
}
47-
48-
Future<bool> _handlePushNotification(RemoteMessage message) async {
49-
try {
50-
final payload = message.data;
51-
// Only handle messages from stream.video
52-
final sender = payload['sender'] as String?;
53-
if (sender != 'stream.video') {
54-
debugPrint('Not a stream.video message');
55-
return false;
56-
}
57-
58-
// Only handle ringing calls.
59-
final type = payload['type'] as String?;
60-
if (type != 'call.ring' && type != 'call.missed') {
61-
debugPrint('Not a call.ring or call.missed message');
62-
return false;
63-
}
64-
65-
// Return if the payload does not contain a call cid.
66-
final callCid = payload['call_cid'] as String?;
67-
if (callCid == null) {
68-
debugPrint('No call cid in payload');
69-
return false;
70-
}
71-
72-
final createdByDisplayName = payload['created_by_display_name'] as String?;
73-
if (createdByDisplayName == null) {
74-
debugPrint('No created_by_display_name in payload');
75-
return false;
76-
}
40+
final subscription = client.observeCallDeclinedRingingEvent();
7741

78-
await showNotification(callCid, type, createdByDisplayName);
79-
return true;
80-
} catch (e, stk) {
81-
debugPrint('Error handling remote message: $e');
82-
debugPrint(stk.toString());
42+
client.disposeAfterResolvingRinging(
43+
disposingCallback: () {
44+
subscription?.cancel();
45+
},
46+
);
8347

84-
return false;
85-
}
48+
await client.handleRingingFlowNotifications(message.data);
8649
}
8750

8851
final ln.FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin =
@@ -151,34 +114,57 @@ class MyApp extends StatefulWidget {
151114
}
152115

153116
class _MyAppState extends State<MyApp> {
117+
final navigatorKey = GlobalKey<NavigatorState>();
118+
154119
@override
155120
void initState() {
156121
super.initState();
157-
158-
_observeFcmMessages();
159122
}
160123

161124
@override
162125
void dispose() {
163-
_fcmSubscription?.cancel();
126+
_compositeSubscription.clear();
164127
super.dispose();
165128
}
166129

167-
StreamSubscription<RemoteMessage>? _fcmSubscription;
168-
169-
Future<void> _observeFcmMessages() async {
170-
// Requests notification permission.
171-
await FirebaseMessaging.instance.requestPermission();
130+
final _compositeSubscription = CompositeSubscription();
131+
132+
void _observeRingingEvents() {
133+
_compositeSubscription.clear();
134+
135+
// On mobile we depend on call kit notifications.
136+
// On desktop and web they are (currently) not available, so we depend on a
137+
// websocket which can receive a call when the app is open.
138+
if (CurrentPlatform.isMobile) {
139+
_compositeSubscription.add(
140+
StreamVideo.instance.observeCoreRingingEvents(
141+
onCallAccepted: _navigateToCall,
142+
),
143+
);
144+
} else {
145+
_compositeSubscription.add(
146+
StreamVideo.instance.state.incomingCall.listen((call) {
147+
if (call == null) return;
148+
149+
// Navigate to the call screen.
150+
_navigateToCall(call);
151+
}),
152+
);
153+
}
154+
}
172155

156+
void _observeFcmMessages() {
173157
FirebaseMessaging.onBackgroundMessage(_onFirebaseBackgroundMessage);
174-
_fcmSubscription = FirebaseMessaging.onMessage.listen(
175-
_onFirebaseForegroundMessage,
176-
);
177158
}
178159

179-
Future<bool> _onFirebaseForegroundMessage(RemoteMessage message) async {
180-
debugPrint('[onFirebaseForegroundMessage] message: ${message.toMap()}');
181-
return _handlePushNotification(message);
160+
void _navigateToCall(Call call) {
161+
navigatorKey.currentState?.push(
162+
MaterialPageRoute<dynamic>(
163+
builder: (context) => StreamCallContainer(
164+
call: call,
165+
),
166+
),
167+
);
182168
}
183169

184170
@override
@@ -188,6 +174,7 @@ class _MyAppState extends State<MyApp> {
188174

189175
return MaterialApp(
190176
title: 'Stream Video Example',
177+
navigatorKey: navigatorKey,
191178
theme: ThemeData(
192179
textTheme: GoogleFonts.robotoMonoTextTheme(),
193180
extensions: <ThemeExtension<dynamic>>[lightAppTheme],
@@ -197,7 +184,16 @@ class _MyAppState extends State<MyApp> {
197184
extensions: <ThemeExtension<dynamic>>[darkAppTheme],
198185
),
199186
themeMode: ThemeMode.dark,
200-
home: LoginScreen(connectUser: widget.connectUser),
187+
home: LoginScreen(
188+
connectUser: (user) async {
189+
await widget.connectUser(user);
190+
191+
_observeRingingEvents();
192+
_observeFcmMessages();
193+
194+
return const Result.success(none);
195+
},
196+
),
201197
debugShowCheckedModeBanner: false,
202198
);
203199
}

0 commit comments

Comments
 (0)