diff --git a/conf_call_sample/README.md b/conf_call_sample/README.md
index 2806ab9..8e6b7f4 100644
--- a/conf_call_sample/README.md
+++ b/conf_call_sample/README.md
@@ -14,7 +14,10 @@ Project contains the following features implemented:
- Screen sharing
- Opponents' mic level monitoring
- Opponents' video bitrate monitoring
-- Speaker/grid modes (the Simulcast feature is applied)
+- Speaker/grid/private modes (the Simulcast feature is applied)
+- CallKit
+- Pre-join screen for video calls
+- Switching from audio call to video without reconnection
## Documentation
@@ -25,6 +28,7 @@ ConnectyCube Conference Calls API documentation - [https://developers.connectycu
## Screenshots
+
@@ -57,6 +61,21 @@ The app will automatically run on the selected iOS device or simulator.
### Run on Linux
- Run command from the Terminal `flutter run -d linux`;
+## Config for the CallKit feature
+The CallKit feature is enabled by default in current version.
+
+The push notification feature is used for implementation the participants notification about the
+new call event. Do the next for configuration:
+
+1. Create your own app in the ConnectyCube admin panel (if not created yet);
+2. Create a project in the Firebase developer console (if not created yet);
+3. Add the Server API key from the Firebase developer console to the ConnectyCube admin panel for the Android platform ([short guide](https://developers.connectycube.com/flutter/push-notifications?id=android));
+4. Add Apple certificate for the iOS platform ([short guide, how to generate and set it to the admin panel](https://developers.connectycube.com/ios/push-notifications?id=create-apns-certificate)). But instead of an APNS certificate, you should choose a VoIP certificate;
+5. Add `google-services.json` file from the Firebase developer console to the Android app by path `conf_call_sample/android/app/`
+6. Configure file `conf_call_sample/lib/src/utils/configs.dart` with your endpoints from the 1st. point of this guide;
+7. Create users in the ConnectyCube admin panel and add them to the configure file `conf_call_sample/lib/src/utils/configs.dart`
+8. Build and run the app as usual;
+
## Can't build yourself?
Got troubles with building Flutter code sample? Just create an issue at [Issues page](https://github.com/ConnectyCube/connectycube-flutter-samples/issues) - we will create the sample for you. For FREE!
diff --git a/conf_call_sample/android/app/build.gradle b/conf_call_sample/android/app/build.gradle
index e3c81bc..226be53 100644
--- a/conf_call_sample/android/app/build.gradle
+++ b/conf_call_sample/android/app/build.gradle
@@ -26,7 +26,7 @@ apply plugin: 'kotlin-android'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
- compileSdkVersion 33
+ compileSdkVersion 34
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
@@ -39,7 +39,7 @@ android {
defaultConfig {
applicationId "com.connectycube.flutter.conference_call_sample"
minSdkVersion 21
- targetSdkVersion 33
+ targetSdkVersion 34
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
@@ -69,3 +69,5 @@ dependencies {
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
}
+
+apply plugin: 'com.google.gms.google-services'
diff --git a/conf_call_sample/android/app/src/main/AndroidManifest.xml b/conf_call_sample/android/app/src/main/AndroidManifest.xml
index 8ffc909..3568691 100644
--- a/conf_call_sample/android/app/src/main/AndroidManifest.xml
+++ b/conf_call_sample/android/app/src/main/AndroidManifest.xml
@@ -17,6 +17,8 @@
+
+
@@ -47,5 +49,15 @@
+
+
+
+
diff --git a/conf_call_sample/android/app/src/main/res/drawable/default_avatar.png b/conf_call_sample/android/app/src/main/res/drawable/default_avatar.png
new file mode 100644
index 0000000..3b19374
Binary files /dev/null and b/conf_call_sample/android/app/src/main/res/drawable/default_avatar.png differ
diff --git a/conf_call_sample/android/app/src/main/res/drawable/ic_notification.xml b/conf_call_sample/android/app/src/main/res/drawable/ic_notification.xml
new file mode 100644
index 0000000..75a088e
--- /dev/null
+++ b/conf_call_sample/android/app/src/main/res/drawable/ic_notification.xml
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/conf_call_sample/android/app/src/main/res/drawable/ic_notification_audio_call.xml b/conf_call_sample/android/app/src/main/res/drawable/ic_notification_audio_call.xml
new file mode 100644
index 0000000..12002fa
--- /dev/null
+++ b/conf_call_sample/android/app/src/main/res/drawable/ic_notification_audio_call.xml
@@ -0,0 +1,10 @@
+
+
+
\ No newline at end of file
diff --git a/conf_call_sample/android/app/src/main/res/drawable/ic_notification_video_call.xml b/conf_call_sample/android/app/src/main/res/drawable/ic_notification_video_call.xml
new file mode 100644
index 0000000..028642d
--- /dev/null
+++ b/conf_call_sample/android/app/src/main/res/drawable/ic_notification_video_call.xml
@@ -0,0 +1,11 @@
+
+
+
+
\ No newline at end of file
diff --git a/conf_call_sample/android/app/src/main/res/raw/custom_ringtone.mp3 b/conf_call_sample/android/app/src/main/res/raw/custom_ringtone.mp3
new file mode 100644
index 0000000..c4e0bbe
Binary files /dev/null and b/conf_call_sample/android/app/src/main/res/raw/custom_ringtone.mp3 differ
diff --git a/conf_call_sample/android/build.gradle b/conf_call_sample/android/build.gradle
index 6fd9850..f6bcc8c 100644
--- a/conf_call_sample/android/build.gradle
+++ b/conf_call_sample/android/build.gradle
@@ -1,5 +1,5 @@
buildscript {
- ext.kotlin_version = '1.6.10'
+ ext.kotlin_version = '1.9.20'
repositories {
google()
jcenter()
@@ -8,6 +8,7 @@ buildscript {
dependencies {
classpath 'com.android.tools.build:gradle:7.3.1'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ classpath 'com.google.gms:google-services:4.3.15'
}
}
@@ -26,6 +27,6 @@ subprojects {
project.evaluationDependsOn(':app')
}
-task clean(type: Delete) {
+tasks.register("clean", Delete) {
delete rootProject.buildDir
}
diff --git a/conf_call_sample/android/gradle.properties b/conf_call_sample/android/gradle.properties
index 38c8d45..94adc3a 100644
--- a/conf_call_sample/android/gradle.properties
+++ b/conf_call_sample/android/gradle.properties
@@ -1,4 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
-android.enableR8=true
android.useAndroidX=true
android.enableJetifier=true
diff --git a/conf_call_sample/assets/audio/calling.mp3 b/conf_call_sample/assets/audio/calling.mp3
new file mode 100644
index 0000000..abfd3c6
Binary files /dev/null and b/conf_call_sample/assets/audio/calling.mp3 differ
diff --git a/conf_call_sample/assets/audio/dialing.mp3 b/conf_call_sample/assets/audio/dialing.mp3
new file mode 100644
index 0000000..b02c987
Binary files /dev/null and b/conf_call_sample/assets/audio/dialing.mp3 differ
diff --git a/conf_call_sample/assets/audio/end_call.mp3 b/conf_call_sample/assets/audio/end_call.mp3
new file mode 100644
index 0000000..f648cca
Binary files /dev/null and b/conf_call_sample/assets/audio/end_call.mp3 differ
diff --git a/conf_call_sample/ios/Conference Calls Sample (Screen Sharing)/Conference Calls Sample (Screen Sharing).entitlements b/conf_call_sample/ios/Conference Calls Sample (Screen Sharing)/Conference Calls Sample (Screen Sharing).entitlements
index 8410518..8c04c8d 100644
--- a/conf_call_sample/ios/Conference Calls Sample (Screen Sharing)/Conference Calls Sample (Screen Sharing).entitlements
+++ b/conf_call_sample/ios/Conference Calls Sample (Screen Sharing)/Conference Calls Sample (Screen Sharing).entitlements
@@ -4,7 +4,7 @@
com.apple.security.application-groups
- group.com.connectycube.flutter
+ group.com.connectycube.flutter.val
diff --git a/conf_call_sample/ios/Conference Calls Sample (Screen Sharing)/SampleHandler.swift b/conf_call_sample/ios/Conference Calls Sample (Screen Sharing)/SampleHandler.swift
index ba4eaae..d56d6e2 100644
--- a/conf_call_sample/ios/Conference Calls Sample (Screen Sharing)/SampleHandler.swift
+++ b/conf_call_sample/ios/Conference Calls Sample (Screen Sharing)/SampleHandler.swift
@@ -9,7 +9,7 @@ import ReplayKit
private enum Constants {
// the App Group ID value that the app and the broadcast extension targets are setup with. It differs for each app.
- static let appGroupIdentifier = "group.com.connectycube.flutter"
+ static let appGroupIdentifier = "group.com.connectycube.flutter.val"
}
class SampleHandler: RPBroadcastSampleHandler {
diff --git a/conf_call_sample/ios/Flutter/AppFrameworkInfo.plist b/conf_call_sample/ios/Flutter/AppFrameworkInfo.plist
index f2872cf..4f8d4d2 100644
--- a/conf_call_sample/ios/Flutter/AppFrameworkInfo.plist
+++ b/conf_call_sample/ios/Flutter/AppFrameworkInfo.plist
@@ -21,6 +21,6 @@
CFBundleVersion
1.0
MinimumOSVersion
- 9.0
+ 11.0
diff --git a/conf_call_sample/ios/Podfile b/conf_call_sample/ios/Podfile
index 313ea4a..2c068c4 100644
--- a/conf_call_sample/ios/Podfile
+++ b/conf_call_sample/ios/Podfile
@@ -1,5 +1,5 @@
# Uncomment this line to define a global platform for your project
-platform :ios, '11.0'
+platform :ios, '12.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
diff --git a/conf_call_sample/ios/Podfile.lock b/conf_call_sample/ios/Podfile.lock
index d857e51..a6823bf 100644
--- a/conf_call_sample/ios/Podfile.lock
+++ b/conf_call_sample/ios/Podfile.lock
@@ -1,42 +1,79 @@
PODS:
+ - assets_audio_player (0.0.1):
+ - Flutter
+ - assets_audio_player_web (0.0.1):
+ - Flutter
+ - connectycube_flutter_call_kit (2.5.0):
+ - Flutter
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- - flutter_webrtc (0.9.20):
+ - flutter_webrtc (0.9.36):
+ - Flutter
+ - WebRTC-SDK (= 114.5735.08)
+ - package_info_plus (0.4.5):
- Flutter
- - WebRTC-SDK (= 104.5112.09)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- - WebRTC-SDK (104.5112.09)
+ - permission_handler_apple (9.1.1):
+ - Flutter
+ - shared_preferences_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
+ - WebRTC-SDK (114.5735.08)
DEPENDENCIES:
+ - assets_audio_player (from `.symlinks/plugins/assets_audio_player/ios`)
+ - assets_audio_player_web (from `.symlinks/plugins/assets_audio_player_web/ios`)
+ - connectycube_flutter_call_kit (from `.symlinks/plugins/connectycube_flutter_call_kit/ios`)
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`)
- - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`)
+ - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
+ - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
+ - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
+ - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
SPEC REPOS:
trunk:
- WebRTC-SDK
EXTERNAL SOURCES:
+ assets_audio_player:
+ :path: ".symlinks/plugins/assets_audio_player/ios"
+ assets_audio_player_web:
+ :path: ".symlinks/plugins/assets_audio_player_web/ios"
+ connectycube_flutter_call_kit:
+ :path: ".symlinks/plugins/connectycube_flutter_call_kit/ios"
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_webrtc:
:path: ".symlinks/plugins/flutter_webrtc/ios"
+ package_info_plus:
+ :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation:
- :path: ".symlinks/plugins/path_provider_foundation/ios"
+ :path: ".symlinks/plugins/path_provider_foundation/darwin"
+ permission_handler_apple:
+ :path: ".symlinks/plugins/permission_handler_apple/ios"
+ shared_preferences_foundation:
+ :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
SPEC CHECKSUMS:
- device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed
+ assets_audio_player: edee322b9cb625571b830b35872ead1a295fd917
+ assets_audio_player_web: 19826380c44375761aa0b9053665c1e3fbc3b86b
+ connectycube_flutter_call_kit: cc0bbb77ddefdc484ae7608eeda046a9bb16f0c7
+ device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea
Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854
- flutter_webrtc: 15c5fb4ea324f3178ff97c0f02b9467b85977e42
- path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
- WebRTC-SDK: 3fa5c6fa717314fade68bffed85737484a28ad0b
+ flutter_webrtc: 55df3aaa802114dad390191a46c2c8d535751268
+ package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85
+ path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
+ permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
+ shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126
+ WebRTC-SDK: c24d2a6c9f571f2ed42297cb8ffba9557093142b
-PODFILE CHECKSUM: 7368163408c647b7eb699d0d788ba6718e18fb8d
+PODFILE CHECKSUM: 4e8f8b2be68aeea4c0d5beb6ff1e79fface1d048
COCOAPODS: 1.11.3
diff --git a/conf_call_sample/ios/Runner.xcodeproj/project.pbxproj b/conf_call_sample/ios/Runner.xcodeproj/project.pbxproj
index 51d2cbd..3196569 100644
--- a/conf_call_sample/ios/Runner.xcodeproj/project.pbxproj
+++ b/conf_call_sample/ios/Runner.xcodeproj/project.pbxproj
@@ -3,7 +3,7 @@
archiveVersion = 1;
classes = {
};
- objectVersion = 50;
+ objectVersion = 54;
objects = {
/* Begin PBXBuildFile section */
@@ -21,6 +21,7 @@
A534AF5D28912D7F003E435B /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = A534AF5828912D7F003E435B /* Atomic.swift */; };
A534AF5E28912D7F003E435B /* SocketConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = A534AF5928912D7F003E435B /* SocketConnection.swift */; };
A534AF5F28912D7F003E435B /* SampleUploader.swift in Sources */ = {isa = PBXBuildFile; fileRef = A534AF5B28912D7F003E435B /* SampleUploader.swift */; };
+ A5BCD48E2AD9459200C37B21 /* custom_ringtone.caf in Resources */ = {isa = PBXBuildFile; fileRef = A5BCD48D2AD9459200C37B21 /* custom_ringtone.caf */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -85,6 +86,7 @@
A534AF5B28912D7F003E435B /* SampleUploader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SampleUploader.swift; sourceTree = ""; };
A534AF6028912E3C003E435B /* Conference Calls Sample (Screen Sharing).entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "Conference Calls Sample (Screen Sharing).entitlements"; sourceTree = ""; };
A534AF6128912E78003E435B /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
+ A5BCD48D2AD9459200C37B21 /* custom_ringtone.caf */ = {isa = PBXFileReference; lastKnownFileType = file; name = custom_ringtone.caf; path = Resources/ringtones/custom_ringtone.caf; sourceTree = ""; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -161,6 +163,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
+ A5BCD48D2AD9459200C37B21 /* custom_ringtone.caf */,
A534AF6128912E78003E435B /* Runner.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
97C146FD1CF9000F007C117D /* Assets.xcassets */,
@@ -204,14 +207,14 @@
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = (
10D3B4B6B08C2EC508498864 /* [CP] Check Pods Manifest.lock */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 78A877BA7019A0897ABF39AE /* [CP] Embed Pods Frameworks */,
+ A534AF5228912C96003E435B /* Embed App Extensions */,
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
- 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
- 78A877BA7019A0897ABF39AE /* [CP] Embed Pods Frameworks */,
- A534AF5228912C96003E435B /* Embed App Extensions */,
);
buildRules = (
);
@@ -247,17 +250,18 @@
isa = PBXProject;
attributes = {
LastSwiftUpdateCheck = 1340;
- LastUpgradeCheck = 1300;
+ LastUpgradeCheck = 1430;
ORGANIZATIONNAME = "The Chromium Authors";
TargetAttributes = {
97C146ED1CF9000F007C117D = {
CreatedOnToolsVersion = 7.3.1;
- DevelopmentTeam = 92G479T7LY;
+ DevelopmentTeam = 6B6Z335W9Z;
LastSwiftMigration = 1100;
+ ProvisioningStyle = Automatic;
};
A534AF4728912C96003E435B = {
CreatedOnToolsVersion = 13.4.1;
- DevelopmentTeam = 92G479T7LY;
+ DevelopmentTeam = 6B6Z335W9Z;
ProvisioningStyle = Automatic;
};
};
@@ -288,6 +292,7 @@
files = (
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ A5BCD48E2AD9459200C37B21 /* custom_ringtone.caf in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
);
@@ -327,10 +332,12 @@
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
);
name = "Thin Binary";
outputPaths = (
@@ -346,14 +353,24 @@
);
inputPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh",
+ "${BUILT_PRODUCTS_DIR}/assets_audio_player/assets_audio_player.framework",
+ "${BUILT_PRODUCTS_DIR}/assets_audio_player_web/assets_audio_player_web.framework",
+ "${BUILT_PRODUCTS_DIR}/connectycube_flutter_call_kit/connectycube_flutter_call_kit.framework",
"${BUILT_PRODUCTS_DIR}/device_info_plus/device_info_plus.framework",
+ "${BUILT_PRODUCTS_DIR}/package_info_plus/package_info_plus.framework",
"${BUILT_PRODUCTS_DIR}/path_provider_foundation/path_provider_foundation.framework",
+ "${BUILT_PRODUCTS_DIR}/shared_preferences_foundation/shared_preferences_foundation.framework",
"${PODS_XCFRAMEWORKS_BUILD_DIR}/WebRTC-SDK/WebRTC.framework/WebRTC",
);
name = "[CP] Embed Pods Frameworks";
outputPaths = (
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/assets_audio_player.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/assets_audio_player_web.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/connectycube_flutter_call_kit.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/device_info_plus.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/package_info_plus.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/path_provider_foundation.framework",
+ "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/shared_preferences_foundation.framework",
"${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/WebRTC.framework",
);
runOnlyForDeploymentPostprocessing = 0;
@@ -363,6 +380,7 @@
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
buildActionMask = 2147483647;
files = (
);
@@ -470,7 +488,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -487,15 +505,17 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 92G479T7LY;
+ DEVELOPMENT_TEAM = 6B6Z335W9Z;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -504,8 +524,9 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.connectycube.flutter.conferenceCallSample;
+ PRODUCT_BUNDLE_IDENTIFIER = com.connectycube.flutter.conferenceCallSample.val;
PRODUCT_NAME = ConferenceCallsSample;
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -559,7 +580,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos;
@@ -608,7 +629,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos;
@@ -626,15 +647,17 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 92G479T7LY;
+ DEVELOPMENT_TEAM = 6B6Z335W9Z;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -643,8 +666,9 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.connectycube.flutter.conferenceCallSample;
+ PRODUCT_BUNDLE_IDENTIFIER = com.connectycube.flutter.conferenceCallSample.val;
PRODUCT_NAME = ConferenceCallsSample;
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
@@ -660,15 +684,17 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
+ CODE_SIGN_IDENTITY = "Apple Development";
+ CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
- DEVELOPMENT_TEAM = 92G479T7LY;
+ DEVELOPMENT_TEAM = 6B6Z335W9Z;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
INFOPLIST_FILE = Runner/Info.plist;
- IPHONEOS_DEPLOYMENT_TARGET = 9.0;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
@@ -677,8 +703,9 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
- PRODUCT_BUNDLE_IDENTIFIER = com.connectycube.flutter.conferenceCallSample;
+ PRODUCT_BUNDLE_IDENTIFIER = com.connectycube.flutter.conferenceCallSample.val;
PRODUCT_NAME = ConferenceCallsSample;
+ PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
VERSIONING_SYSTEM = "apple-generic";
@@ -697,7 +724,7 @@
CODE_SIGN_ENTITLEMENTS = "Conference Calls Sample (Screen Sharing)/Conference Calls Sample (Screen Sharing).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 0.3.1;
- DEVELOPMENT_TEAM = 92G479T7LY;
+ DEVELOPMENT_TEAM = 6B6Z335W9Z;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Conference Calls Sample (Screen Sharing)/Info.plist";
@@ -712,7 +739,7 @@
MARKETING_VERSION = 0.3.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
- PRODUCT_BUNDLE_IDENTIFIER = "com.connectycube.flutter.conferenceCallSample.Conference-Calls-Sample-Screen-Sharing";
+ PRODUCT_BUNDLE_IDENTIFIER = "com.connectycube.flutter.conferenceCallSample.val.Conference-Calls-Sample-Screen-Sharing.val";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
@@ -735,7 +762,7 @@
CODE_SIGN_ENTITLEMENTS = "Conference Calls Sample (Screen Sharing)/Conference Calls Sample (Screen Sharing).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 0.3.1;
- DEVELOPMENT_TEAM = 92G479T7LY;
+ DEVELOPMENT_TEAM = 6B6Z335W9Z;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Conference Calls Sample (Screen Sharing)/Info.plist";
@@ -749,7 +776,7 @@
);
MARKETING_VERSION = 0.3.1;
MTL_FAST_MATH = YES;
- PRODUCT_BUNDLE_IDENTIFIER = "com.connectycube.flutter.conferenceCallSample.Conference-Calls-Sample-Screen-Sharing";
+ PRODUCT_BUNDLE_IDENTIFIER = "com.connectycube.flutter.conferenceCallSample.val.Conference-Calls-Sample-Screen-Sharing.val";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
@@ -770,7 +797,7 @@
CODE_SIGN_ENTITLEMENTS = "Conference Calls Sample (Screen Sharing)/Conference Calls Sample (Screen Sharing).entitlements";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 0.3.1;
- DEVELOPMENT_TEAM = 92G479T7LY;
+ DEVELOPMENT_TEAM = 6B6Z335W9Z;
GCC_C_LANGUAGE_STANDARD = gnu11;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Conference Calls Sample (Screen Sharing)/Info.plist";
@@ -784,7 +811,7 @@
);
MARKETING_VERSION = 0.3.1;
MTL_FAST_MATH = YES;
- PRODUCT_BUNDLE_IDENTIFIER = "com.connectycube.flutter.conferenceCallSample.Conference-Calls-Sample-Screen-Sharing";
+ PRODUCT_BUNDLE_IDENTIFIER = "com.connectycube.flutter.conferenceCallSample.val.Conference-Calls-Sample-Screen-Sharing.val";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
diff --git a/conf_call_sample/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/conf_call_sample/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
index d477454..57081d2 100644
--- a/conf_call_sample/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
+++ b/conf_call_sample/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -1,6 +1,6 @@
-
+
+
+
-
+
+
@@ -14,13 +16,14 @@
-
+
-
+
+
diff --git a/conf_call_sample/ios/Runner/Info.plist b/conf_call_sample/ios/Runner/Info.plist
index ed99a79..00220ca 100644
--- a/conf_call_sample/ios/Runner/Info.plist
+++ b/conf_call_sample/ios/Runner/Info.plist
@@ -2,6 +2,8 @@
+ CADisableMinimumFrameDurationOnPhone
+
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleExecutable
@@ -22,6 +24,22 @@
$(FLUTTER_BUILD_NUMBER)
LSRequiresIPhoneOS
+ NSCameraUsageDescription
+ $(PRODUCT_NAME) Camera Usage!
+ NSMicrophoneUsageDescription
+ $(PRODUCT_NAME) Microphone Usage!
+ RTCAppGroupIdentifier
+ group.com.connectycube.flutter.val
+ RTCScreenSharingExtension
+ com.connectycube.flutter.conferenceCallSample.val.Conference-Calls-Sample-Screen-Sharing.val
+ UIBackgroundModes
+
+ audio
+ fetch
+ processing
+ remote-notification
+ voip
+
UILaunchStoryboardName
LaunchScreen
UIMainStoryboardFile
@@ -41,20 +59,7 @@
UIViewControllerBasedStatusBarAppearance
- NSCameraUsageDescription
- $(PRODUCT_NAME) Camera Usage!
- NSMicrophoneUsageDescription
- $(PRODUCT_NAME) Microphone Usage!
- UIBackgroundModes
-
- voip
- audio
-
- RTCScreenSharingExtension
- com.connectycube.flutter.conferenceCallSample.Conference-Calls-Sample-Screen-Sharing
- RTCAppGroupIdentifier
- group.com.connectycube.flutter
- CADisableMinimumFrameDurationOnPhone
+ UIApplicationSupportsIndirectInputEvents
diff --git a/conf_call_sample/ios/Runner/Resources/ringtones/custom_ringtone.caf b/conf_call_sample/ios/Runner/Resources/ringtones/custom_ringtone.caf
new file mode 100644
index 0000000..0c7456d
Binary files /dev/null and b/conf_call_sample/ios/Runner/Resources/ringtones/custom_ringtone.caf differ
diff --git a/conf_call_sample/ios/Runner/Runner.entitlements b/conf_call_sample/ios/Runner/Runner.entitlements
index 8410518..a6b77e4 100644
--- a/conf_call_sample/ios/Runner/Runner.entitlements
+++ b/conf_call_sample/ios/Runner/Runner.entitlements
@@ -2,9 +2,11 @@
+ aps-environment
+ development
com.apple.security.application-groups
- group.com.connectycube.flutter
+ group.com.connectycube.flutter.val
diff --git a/conf_call_sample/lib/main.dart b/conf_call_sample/lib/main.dart
index 4191736..52426c7 100644
--- a/conf_call_sample/lib/main.dart
+++ b/conf_call_sample/lib/main.dart
@@ -1,9 +1,11 @@
+import 'package:conf_call_sample/src/managers/call_manager.dart';
import 'package:flutter/material.dart';
import 'package:connectycube_sdk/connectycube_sdk.dart';
-import 'src//utils/configs.dart' as config;
-import 'src/login_screen.dart';
+import 'src/utils/configs.dart' as config;
+import 'src/utils/pref_util.dart';
+import 'src/screens/login_screen.dart';
void main() => runApp(App());
@@ -17,6 +19,8 @@ class App extends StatefulWidget {
class _AppState extends State {
@override
Widget build(BuildContext context) {
+ initCallManager(context);
+
return MaterialApp(
theme: ThemeData(
primarySwatch: Colors.green,
@@ -28,12 +32,48 @@ class _AppState extends State {
@override
void initState() {
super.initState();
- init(
- config.APP_ID,
- config.AUTH_KEY,
- config.AUTH_SECRET,
- );
- // setEndpoints('https://', '');
+ initConnectycube();
+ }
+
+ void initCallManager(BuildContext context) {
+ SharedPrefs.getUser().then((savedUser) {
+ if(savedUser != null){
+ CallManager.instance.init(context);
+ }
+ });
}
}
+
+initConnectycube() {
+ init(
+ config.APP_ID,
+ config.AUTH_KEY,
+ config.AUTH_SECRET,
+ onSessionRestore: () {
+ return SharedPrefs.getUser().then((savedUser) {
+ return createSession(savedUser);
+ });
+ },
+ );
+
+ setEndpoints(config.API_ENDPOINT, config.CHAT_ENDPOINT);
+
+ ConferenceConfig.instance.url = config.CONF_SERVER_ENDPOINT;
+}
+
+initConnectycubeContextLess() async {
+ CubeSettings.instance.applicationId = config.APP_ID;
+ CubeSettings.instance.authorizationKey = config.AUTH_KEY;
+ CubeSettings.instance.authorizationSecret = config.AUTH_SECRET;
+ CubeSettings.instance.onSessionRestore = () {
+ return SharedPrefs.getUser().then((savedUser) {
+ return createSession(savedUser);
+ });
+ };
+
+ CubeSettings.instance.apiEndpoint = config.API_ENDPOINT;
+ CubeSettings.instance.chatEndpoint = config.CHAT_ENDPOINT;
+
+ ConferenceConfig.instance.url = config.CONF_SERVER_ENDPOINT;
+}
diff --git a/conf_call_sample/lib/src/call_screen.dart b/conf_call_sample/lib/src/call_screen.dart
deleted file mode 100644
index 9b6b237..0000000
--- a/conf_call_sample/lib/src/call_screen.dart
+++ /dev/null
@@ -1,1174 +0,0 @@
-import 'dart:io';
-
-import 'package:flutter/foundation.dart';
-import 'package:flutter/material.dart';
-
-import 'package:connectycube_sdk/connectycube_sdk.dart';
-import 'package:flutter_speed_dial/flutter_speed_dial.dart';
-import 'package:web_browser_detect/web_browser_detect.dart';
-
-import 'utils/call_manager.dart';
-import 'utils/configs.dart';
-import 'utils/platform_utils.dart';
-import 'utils/speakers_manager.dart';
-import 'utils/video_config.dart';
-
-class IncomingCallScreen extends StatelessWidget {
- static const String TAG = "IncomingCallScreen";
- final String _meetingId;
- final List _participantIds;
-
- IncomingCallScreen(this._meetingId, this._participantIds);
-
- @override
- Widget build(BuildContext context) {
- CallManager.instance.onCloseCall = () {
- log("onCloseCall", TAG);
- Navigator.pop(context);
- };
-
- return WillPopScope(
- onWillPop: () => _onBackPressed(context),
- child: Scaffold(
- body: Center(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: EdgeInsets.all(36),
- child: Text(_getCallTitle(), style: TextStyle(fontSize: 28)),
- ),
- Padding(
- padding: EdgeInsets.only(top: 36, bottom: 8),
- child: Text("Members:", style: TextStyle(fontSize: 20)),
- ),
- Padding(
- padding: EdgeInsets.only(bottom: 86),
- child: Text(_participantIds.join(", "),
- style: TextStyle(fontSize: 18)),
- ),
- Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: EdgeInsets.only(right: 36),
- child: FloatingActionButton(
- heroTag: "RejectCall",
- child: Icon(
- Icons.call_end,
- color: Colors.white,
- ),
- backgroundColor: Colors.red,
- onPressed: () => _rejectCall(context),
- ),
- ),
- Padding(
- padding: EdgeInsets.only(left: 36),
- child: FloatingActionButton(
- heroTag: "AcceptCall",
- child: Icon(
- Icons.call,
- color: Colors.white,
- ),
- backgroundColor: Colors.green,
- onPressed: () => _acceptCall(context),
- ),
- ),
- ],
- ),
- ],
- ),
- ),
- ),
- );
- }
-
- _getCallTitle() {
- String callType = "Video";
- return "Incoming $callType call";
- }
-
- void _acceptCall(BuildContext context) async {
- ConferenceSession callSession = await ConferenceClient.instance
- .createCallSession(CubeChatConnection.instance.currentUser!.id!);
- Navigator.pushReplacement(
- context,
- MaterialPageRoute(
- builder: (context) => ConversationCallScreen(
- callSession, _meetingId, _participantIds, true),
- ),
- );
- }
-
- void _rejectCall(BuildContext context) {
- CallManager.instance.reject(_meetingId, false);
- Navigator.pop(context);
- }
-
- Future _onBackPressed(BuildContext context) {
- return Future.value(false);
- }
-}
-
-class ConversationCallScreen extends StatefulWidget {
- final ConferenceSession _callSession;
- final String _meetingId;
- final List opponents;
- final bool _isIncoming;
-
- @override
- State createState() {
- return _ConversationCallScreenState(
- _callSession, _meetingId, opponents, _isIncoming);
- }
-
- ConversationCallScreen(
- this._callSession, this._meetingId, this.opponents, this._isIncoming);
-}
-
-class _ConversationCallScreenState extends State {
- static const String TAG = "_ConversationCallScreenState";
- final ConferenceSession _callSession;
- CallManager _callManager = CallManager.instance;
- final bool _isIncoming;
- final String _meetingId;
- final List _opponents;
- final CubeStatsReportsManager _statsReportsManager =
- CubeStatsReportsManager();
- final SpeakersManager _speakersManager = SpeakersManager();
-
- LayoutMode layoutMode = LayoutMode.speaker;
- bool _isCameraEnabled = true;
- bool _isSpeakerEnabled = true;
- bool _isMicMute = false;
- bool _enableScreenSharing;
- bool _isFrontCameraUsed = true;
- RTCVideoViewObjectFit primaryVideoFit =
- RTCVideoViewObjectFit.RTCVideoViewObjectFitCover;
- final int currentUserId = CubeChatConnection.instance.currentUser!.id!;
-
- MapEntry? primaryRenderer;
- Map minorRenderers = {};
-
- _ConversationCallScreenState(
- this._callSession, this._meetingId, this._opponents, this._isIncoming)
- : _enableScreenSharing = !_callSession.startScreenSharing;
-
- @override
- void initState() {
- super.initState();
- _initCustomMediaConfigs();
- _statsReportsManager.init(_callSession);
- _speakersManager.init(_statsReportsManager, _onSpeakerChanged);
- _callManager.onReceiveRejectCall = _onReceiveRejectCall;
- _callManager.onCloseCall = _onCloseCall;
-
- _callSession.onLocalStreamReceived = _addLocalMediaStream;
- _callSession.onRemoteStreamTrackReceived = _addRemoteMediaStream;
- _callSession.onSessionClosed = _onSessionClosed;
- _callSession.onPublishersReceived = onPublishersReceived;
- _callSession.onPublisherLeft = onPublisherLeft;
- _callSession.onError = onError;
- _callSession.onSubStreamChanged = onSubStreamChanged;
- _callSession.onLayerChanged = onLayerChanged;
-
- _callSession.joinDialog(_meetingId, ((publishers) {
- log("join session= $publishers", TAG);
-
- if (!_isIncoming) {
- _callManager.startCall(
- _meetingId, _opponents, _callSession.currentUserId);
- }
- }), conferenceRole: ConferenceRole.PUBLISHER);
- // }), conferenceRole: ConferenceRole.LISTENER);
- }
-
- @override
- void dispose() {
- super.dispose();
-
- _statsReportsManager.dispose();
- _speakersManager.dispose();
-
- stopBackgroundExecution();
-
- primaryRenderer?.value.srcObject = null;
- primaryRenderer?.value.dispose();
-
- minorRenderers.forEach((opponentId, renderer) {
- log("[dispose] dispose renderer for $opponentId", TAG);
- try {
- renderer.srcObject = null;
- renderer.dispose();
- } catch (e) {
- log('Error $e');
- }
- });
- }
-
- void _onCloseCall() {
- log("_onCloseCall", TAG);
- _callSession.leave();
- }
-
- void _onReceiveRejectCall(String meetingId, int participantId, bool isBusy) {
- log("_onReceiveRejectCall got reject from user $participantId", TAG);
- }
-
- Future _addLocalMediaStream(MediaStream stream) async {
- log("_addLocalMediaStream", TAG);
- _callSession.setMaxBandwidth(0);
-
- _addMediaStream(currentUserId, true, stream);
- }
-
- void _addRemoteMediaStream(session, int userId, MediaStream stream,
- {String? trackId}) {
- log("_addRemoteMediaStream for user $userId", TAG);
-
- _addMediaStream(userId, false, stream, trackId: trackId);
- }
-
- void _removeMediaStream(callSession, int userId) {
- log("_removeMediaStream for user $userId", TAG);
- RTCVideoRenderer? videoRenderer = minorRenderers[userId];
- if (videoRenderer != null) {
- videoRenderer.srcObject = null;
- videoRenderer.dispose();
-
- setState(() {
- minorRenderers.remove(userId);
- });
- } else if (primaryRenderer?.key == userId) {
- var rendererToRemove = primaryRenderer?.value;
-
- if (rendererToRemove != null) {
- rendererToRemove.srcObject = null;
- rendererToRemove.dispose();
- }
-
- if (minorRenderers.isNotEmpty) {
- setState(() {
- var userIdToRemoveRenderer = minorRenderers.keys.firstWhere(
- (key) => key != currentUserId,
- orElse: () => minorRenderers.keys.first);
-
- primaryRenderer = MapEntry(userIdToRemoveRenderer,
- minorRenderers.remove(userIdToRemoveRenderer)!);
- _chooseOpponentsStreamsQuality(
- {userIdToRemoveRenderer: StreamType.high});
- });
- }
- }
- }
-
- void _closeSessionIfLast() {
- if (_callSession.allActivePublishers.length < 1) {
- _callManager.stopCall();
- _callSession.leave();
- }
- }
-
- void _onSessionClosed(session) {
- log("_onSessionClosed", TAG);
- _statsReportsManager.dispose();
- Navigator.pop(context);
- }
-
- void onPublishersReceived(publishers) {
- log("onPublishersReceived", TAG);
- handlePublisherReceived(publishers);
- }
-
- void onPublisherLeft(publisher) {
- log("onPublisherLeft $publisher", TAG);
- _removeMediaStream(_callSession, publisher);
- _closeSessionIfLast();
- }
-
- void onError(ex) {
- log("onError $ex", TAG);
- }
-
- void onSubStreamChanged(int userId, StreamType streamType) {
- log("onSubStreamChanged userId: $userId, streamType: $streamType", TAG);
- }
-
- void onLayerChanged(int userId, int layer) {
- log("onLayerChanged userId: $userId, layer: $layer", TAG);
- }
-
- void _addMediaStream(int userId, bool isLocalStream, MediaStream stream,
- {String? trackId}) async {
- log("_addMediaStream for user $userId", TAG);
- if (primaryRenderer == null || primaryRenderer!.key == currentUserId) {
- if (primaryRenderer == null) {
- primaryRenderer = MapEntry(userId, RTCVideoRenderer());
- await primaryRenderer!.value.initialize();
-
- setState(() {
- _setSourceForRenderer(primaryRenderer!.value, stream, isLocalStream,
- trackId: trackId);
- });
- } else {
- var newRender = RTCVideoRenderer();
- await newRender.initialize();
-
- _setSourceForRenderer(newRender, stream, isLocalStream,
- trackId: trackId);
-
- setState(() {
- minorRenderers.addEntries([primaryRenderer!]);
-
- primaryRenderer = MapEntry(userId, newRender);
- });
- }
-
- _chooseOpponentsStreamsQuality({primaryRenderer!.key: StreamType.high});
- } else {
- var newRender = primaryRenderer?.value;
-
- if (newRender != null && userId == primaryRenderer?.key) {
- _setSourceForRenderer(newRender, stream, isLocalStream,
- trackId: trackId);
-
- return;
- }
-
- newRender = minorRenderers[userId];
-
- if (newRender == null) {
- newRender = RTCVideoRenderer();
- await newRender.initialize();
- }
-
- _setSourceForRenderer(newRender, stream, isLocalStream, trackId: trackId);
-
- if (!minorRenderers.containsKey(userId)) {
- _chooseOpponentsStreamsQuality({userId: StreamType.low});
-
- setState(() {
- minorRenderers[userId] = newRender!;
- });
- }
- }
- }
-
- _setSourceForRenderer(
- RTCVideoRenderer renderer, MediaStream stream, bool isLocalStream,
- {String? trackId}) {
- isLocalStream
- ? renderer.srcObject = stream
- : renderer.setSrcObject(stream: stream, trackId: trackId);
- }
-
- void handlePublisherReceived(List publishers) {
- if (!_isIncoming) {
- publishers.forEach((id) {
- if (id != null) {
- _callManager.handleAcceptCall(id);
- }
- });
- }
- }
-
- List renderSpeakerModeViews(Orientation orientation) {
- List streamsExpanded = [];
-
- if (primaryRenderer != null) {
- streamsExpanded.add(
- Expanded(
- flex: 3,
- child: Stack(
- children: [
- RTCVideoView(
- primaryRenderer!.value,
- objectFit: primaryVideoFit,
- mirror: primaryRenderer!.key == currentUserId &&
- _isFrontCameraUsed &&
- _enableScreenSharing,
- ),
- Align(
- alignment: Alignment.topRight,
- child: Padding(
- padding: orientation == Orientation.portrait
- ? EdgeInsets.only(top: 40, right: 20)
- : EdgeInsets.only(right: 20, top: 20),
- child: FloatingActionButton(
- elevation: 0,
- heroTag: "ToggleScreenFit",
- child: Icon(
- primaryVideoFit ==
- RTCVideoViewObjectFit.RTCVideoViewObjectFitCover
- ? Icons.zoom_in_map
- : Icons.zoom_out_map,
- color: Colors.white,
- ),
- onPressed: () => _switchPrimaryVideoFit(),
- backgroundColor: Colors.black38,
- ),
- ),
- ),
- if (primaryRenderer!.key != currentUserId)
- Align(
- alignment: Alignment.topCenter,
- child: Container(
- margin: EdgeInsets.only(
- top: MediaQuery.of(context).padding.top + 8),
- child: ClipRRect(
- borderRadius: BorderRadius.all(Radius.circular(12)),
- child: Container(
- padding: EdgeInsets.all(6),
- color: Colors.black26,
- child: StreamBuilder(
- stream: _statsReportsManager.videoBitrateStream.where(
- (event) => event.userId == primaryRenderer!.key),
- builder: (context, snapshot) {
- if (!snapshot.hasData) {
- return Text(
- '0 kbits/sec',
- style: TextStyle(color: Colors.white),
- );
- } else {
- var videoBitrateForUser = snapshot.data!;
- return Text(
- '${videoBitrateForUser.bitRate} kbits/sec',
- style: TextStyle(color: Colors.white),
- );
- }
- },
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- );
- }
-
- var itemHeight;
- var itemWidth;
-
- if (orientation == Orientation.portrait) {
- itemHeight = MediaQuery.of(context).size.height / 3 * 0.8;
- itemWidth = itemHeight / 3 * 4;
- } else {
- itemWidth = MediaQuery.of(context).size.width / 3 * 0.8;
- itemHeight = itemWidth / 4 * 3;
- }
-
- var minorItems = buildItems(minorRenderers, itemWidth, itemHeight);
-
- if (minorRenderers.isNotEmpty) {
- var membersList = Expanded(
- flex: 1,
- child: ListView(
- scrollDirection: orientation == Orientation.landscape
- ? Axis.vertical
- : Axis.horizontal,
- children: minorItems,
- ),
- );
-
- streamsExpanded.add(membersList);
- }
-
- return streamsExpanded;
- }
-
- void _updatePrimaryUser(int userId, bool force) {
- if (userId == primaryRenderer!.key ||
- (userId == currentUserId && !force) ||
- layoutMode == LayoutMode.grid) return;
-
- _chooseOpponentsStreamsQuality({
- userId: StreamType.high,
- primaryRenderer!.key: StreamType.low,
- });
-
- setState(() {
- minorRenderers.addEntries([primaryRenderer!]);
- primaryRenderer =
- minorRenderers.entries.where((entry) => entry.key == userId).first;
- minorRenderers.remove(userId);
- });
- }
-
- void _onSpeakerChanged(int userId) {
- if (userId == currentUserId) return;
-
- _updatePrimaryUser(userId, false);
- }
-
- @override
- Widget build(BuildContext context) {
- return WillPopScope(
- onWillPop: () => _onBackPressed(context),
- child: Stack(
- children: [
- Scaffold(
- backgroundColor: Colors.grey,
- body: _isVideoCall()
- ? OrientationBuilder(
- builder: (context, orientation) {
- return layoutMode == LayoutMode.speaker
- ? _buildSpeakerModLayout(orientation)
- : _buildGridModLayout(orientation);
- },
- )
- : Center(
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: EdgeInsets.only(bottom: 24),
- child: Text(
- "Audio call",
- style: TextStyle(fontSize: 28),
- ),
- ),
- Padding(
- padding: EdgeInsets.only(bottom: 12),
- child: Text(
- "Members:",
- style: TextStyle(
- fontSize: 20, fontStyle: FontStyle.italic),
- ),
- ),
- Text(
- _callSession.allActivePublishers.join(", "),
- style: TextStyle(fontSize: 20),
- ),
- ],
- ),
- ),
- ),
- Align(
- alignment: Alignment.bottomCenter,
- child: _getActionsPanel(),
- ),
- OrientationBuilder(
- builder: (context, orientation) {
- return Align(
- alignment: Alignment.topLeft,
- child: Container(
- margin: orientation == Orientation.portrait
- ? EdgeInsets.only(top: 40, left: 20)
- : EdgeInsets.only(left: 40, top: 20),
- child: FloatingActionButton(
- elevation: 0,
- heroTag: "ToggleScreenMode",
- child: Icon(
- layoutMode == LayoutMode.speaker
- ? Icons.grid_view_rounded
- : Icons.view_sidebar_rounded,
- color: Colors.white,
- ),
- onPressed: () => _switchLayoutMode(),
- backgroundColor: Colors.black38,
- ),
- ),
- );
- },
- ),
- ],
- ),
- );
- }
-
- _switchLayoutMode() {
- setState(() {
- layoutMode = layoutMode == LayoutMode.speaker
- ? LayoutMode.grid
- : LayoutMode.speaker;
-
- var config;
- if (layoutMode == LayoutMode.grid) {
- config = Map.fromEntries(minorRenderers
- .map((key, value) => MapEntry(key, StreamType.medium))
- .entries);
- config.addEntries([MapEntry(primaryRenderer!.key, StreamType.medium)]);
- } else {
- config = Map.fromEntries(minorRenderers
- .map((key, value) => MapEntry(key, StreamType.low))
- .entries);
- config.addEntries([MapEntry(primaryRenderer!.key, StreamType.high)]);
- }
-
- _chooseOpponentsStreamsQuality(config);
- });
- }
-
- Widget _buildSpeakerModLayout(Orientation orientation) {
- return Center(
- child: Container(
- child: orientation == Orientation.portrait
- ? Column(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: renderSpeakerModeViews(orientation))
- : Row(
- mainAxisAlignment: MainAxisAlignment.spaceEvenly,
- children: renderSpeakerModeViews(orientation)),
- ),
- );
- }
-
- Widget _buildGridModLayout(Orientation orientation) {
- return Container(
- margin: MediaQuery.of(context).padding,
- child: GridView(
- gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
- crossAxisCount: orientation == Orientation.portrait ? 2 : 4,
- crossAxisSpacing: 0,
- mainAxisSpacing: 0,
- childAspectRatio: 4 / 3,
- ),
- padding: EdgeInsets.all(8),
- scrollDirection: Axis.vertical,
- children: _buildGridItems(orientation),
- ),
- );
- }
-
- List _buildGridItems(Orientation orientation) {
- Map allRenderers =
- Map.fromEntries([...minorRenderers.entries]);
- if (primaryRenderer != null) {
- allRenderers.addEntries([primaryRenderer!]);
- }
- var itemHeight;
- var itemWidth;
-
- if (orientation == Orientation.portrait) {
- itemWidth = MediaQuery.of(context).size.width * 0.95 / 2;
- itemHeight = itemWidth / 4 * 3;
- } else {
- itemWidth = MediaQuery.of(context).size.width * 0.95 / 2;
- itemHeight = itemWidth / 4 * 3;
- }
-
- return buildItems(allRenderers, itemWidth, itemHeight);
- }
-
- List buildItems(Map renderers,
- double itemWidth, double itemHeight) {
- return renderers.entries
- .map(
- (entry) => GestureDetector(
- onDoubleTap: () => _updatePrimaryUser(entry.key, true),
- child: SizedBox(
- width: itemWidth,
- height: itemHeight,
- child: Padding(
- padding: EdgeInsets.all(4),
- child: Stack(
- children: [
- Container(
- margin: EdgeInsets.all(4),
- decoration: ShapeDecoration(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(2.0),
- side: BorderSide(
- width: 4,
- color: Colors.white70,
- ),
- ),
- ),
- child: Stack(
- children: [
- StreamBuilder(
- stream: _statsReportsManager.micLevelStream
- .where((event) => event.userId == entry.key),
- builder: (context, snapshot) {
- var width = !snapshot.hasData
- ? 0
- : snapshot.data!.micLevel * 4;
-
- return Container(
- decoration: ShapeDecoration(
- shape: RoundedRectangleBorder(
- borderRadius: BorderRadius.circular(2.0),
- side: BorderSide(
- width: width.toDouble(),
- color: Colors.green,
- strokeAlign: 1.0),
- ),
- ),
- );
- },
- ),
- RTCVideoView(
- entry.value,
- objectFit: RTCVideoViewObjectFit
- .RTCVideoViewObjectFitCover,
- mirror: entry.key == currentUserId &&
- _isFrontCameraUsed &&
- _enableScreenSharing,
- ),
- ],
- ),
- ),
- if (entry.key != currentUserId)
- Align(
- alignment: Alignment.topCenter,
- child: Container(
- margin: EdgeInsets.only(top: 8),
- child: ClipRRect(
- borderRadius:
- BorderRadius.all(Radius.circular(12)),
- child: Container(
- padding: EdgeInsets.all(6),
- color: Colors.black26,
- child: StreamBuilder(
- stream: _statsReportsManager
- .videoBitrateStream
- .where(
- (event) => event.userId == entry.key),
- builder: (context, snapshot) {
- if (!snapshot.hasData) {
- return Text(
- '0 kbits/sec',
- style: TextStyle(color: Colors.white),
- );
- } else {
- var videoBitrateForUser = snapshot.data!;
- return Text(
- '${videoBitrateForUser.bitRate} kbits/sec',
- style: TextStyle(color: Colors.white),
- );
- }
- },
- ),
- ),
- ),
- )),
- Align(
- alignment: Alignment.bottomCenter,
- child: Container(
- margin: EdgeInsets.only(bottom: 8),
- child: ClipRRect(
- borderRadius: BorderRadius.all(Radius.circular(12)),
- child: Container(
- padding: EdgeInsets.all(6),
- color: Colors.black26,
- child: Text(
- entry.key ==
- CubeChatConnection
- .instance.currentUser?.id
- ? 'Me'
- : users
- .where((user) => user.id == entry.key)
- .first
- .fullName ??
- 'Unknown',
- style: TextStyle(color: Colors.white),
- ),
- ),
- ),
- ),
- ),
- ],
- ),
- ),
- ),
- ),
- )
- .toList();
- }
-
- Widget _getActionsPanel() {
- return Container(
- margin: EdgeInsets.only(bottom: 16, left: 8, right: 8),
- child: ClipRRect(
- borderRadius: BorderRadius.only(
- bottomLeft: Radius.circular(32),
- bottomRight: Radius.circular(32),
- topLeft: Radius.circular(32),
- topRight: Radius.circular(32)),
- child: Container(
- padding: EdgeInsets.all(4),
- color: Colors.black26,
- child: Row(
- mainAxisSize: MainAxisSize.min,
- children: [
- Padding(
- padding: EdgeInsets.only(right: 4),
- child: FloatingActionButton(
- elevation: 0,
- heroTag: "Mute",
- child: Icon(
- _isMicMute ? Icons.mic_off : Icons.mic,
- color: _isMicMute ? Colors.grey : Colors.white,
- ),
- onPressed: () => _muteMic(),
- backgroundColor: Colors.black38,
- ),
- ),
- Visibility(
- visible: _enableScreenSharing,
- child: Padding(
- padding: EdgeInsets.only(right: 4),
- child: FloatingActionButton(
- elevation: 0,
- heroTag: "ToggleCamera",
- child: Icon(
- _isVideoEnabled() ? Icons.videocam : Icons.videocam_off,
- color: _isVideoEnabled() ? Colors.white : Colors.grey,
- ),
- onPressed: () => _toggleCamera(),
- backgroundColor: Colors.black38,
- ),
- ),
- ),
- SpeedDial(
- heroTag: "Options",
- icon: Icons.more_vert,
- activeIcon: Icons.close,
- backgroundColor: Colors.black38,
- switchLabelPosition: true,
- overlayColor: Colors.black,
- elevation: 0,
- overlayOpacity: 0.5,
- children: [
- SpeedDialChild(
- child: Icon(
- _enableScreenSharing
- ? Icons.screen_share
- : Icons.stop_screen_share,
- color: Colors.white,
- ),
- backgroundColor: Colors.black38,
- foregroundColor: Colors.white,
- label:
- '${_enableScreenSharing ? 'Start' : 'Stop'} Screen Sharing',
- onTap: () => _toggleScreenSharing(),
- ),
- SpeedDialChild(
- visible: !(kIsWeb &&
- (Browser().browserAgent == BrowserAgent.Safari ||
- Browser().browserAgent == BrowserAgent.Firefox)),
- child: Icon(
- kIsWeb || WebRTC.platformIsDesktop
- ? Icons.surround_sound
- : _isSpeakerEnabled
- ? Icons.volume_up
- : Icons.volume_off,
- color: _isSpeakerEnabled ? Colors.white : Colors.grey,
- ),
- backgroundColor: Colors.black38,
- foregroundColor: Colors.white,
- label:
- 'Switch ${kIsWeb || WebRTC.platformIsDesktop ? 'Audio output' : 'Speakerphone'}',
- onTap: () => _switchSpeaker(),
- ),
- SpeedDialChild(
- visible: kIsWeb || WebRTC.platformIsDesktop,
- child: Icon(
- Icons.record_voice_over,
- color: Colors.white,
- ),
- backgroundColor: Colors.black38,
- foregroundColor: Colors.white,
- label: 'Switch Audio Input device',
- onTap: () => _switchAudioInput(),
- ),
- SpeedDialChild(
- visible: _enableScreenSharing,
- child: Icon(
- Icons.cameraswitch,
- color: _isVideoEnabled() ? Colors.white : Colors.grey,
- ),
- backgroundColor: Colors.black38,
- foregroundColor: Colors.white,
- label: 'Switch Camera',
- onTap: () => _switchCamera(),
- ),
- ],
- ),
- Expanded(
- child: SizedBox(),
- flex: 1,
- ),
- Padding(
- padding: EdgeInsets.only(left: 0),
- child: FloatingActionButton(
- child: Icon(
- Icons.call_end,
- color: Colors.white,
- ),
- backgroundColor: Colors.red,
- onPressed: () => _endCall(),
- ),
- ),
- ],
- ),
- ),
- ),
- );
- }
-
- _endCall() {
- _callManager.stopCall();
- _callSession.leave();
- }
-
- Future _onBackPressed(BuildContext context) {
- return Future.value(false);
- }
-
- _chooseOpponentsStreamsQuality(Map config) {
- config.remove(currentUserId);
-
- if (config.isEmpty) return;
-
- _callSession.requestPreferredStreamsForOpponents(config);
- }
-
- _muteMic() {
- setState(() {
- _isMicMute = !_isMicMute;
- _callSession.setMicrophoneMute(_isMicMute);
- });
- }
-
- _switchCamera() {
- if (!_isVideoEnabled()) return;
-
- if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
- _callSession.switchCamera().then((isFrontCameraUsed) {
- setState(() {
- _isFrontCameraUsed = isFrontCameraUsed;
- });
- });
- } else {
- showDialog(
- context: context,
- builder: (BuildContext context) {
- return FutureBuilder>(
- future: _callSession.getCameras(),
- builder: (context, snapshot) {
- if (!snapshot.hasData || snapshot.data!.isEmpty) {
- return AlertDialog(
- content: const Text('No cameras found'),
- actions: [
- TextButton(
- style: TextButton.styleFrom(
- textStyle: Theme.of(context).textTheme.labelLarge,
- ),
- child: const Text('Ok'),
- onPressed: () {
- Navigator.of(context).pop();
- },
- )
- ],
- );
- } else {
- return SimpleDialog(
- title: const Text('Select camera'),
- children: snapshot.data?.map(
- (mediaDeviceInfo) {
- return SimpleDialogOption(
- onPressed: () {
- Navigator.pop(context, mediaDeviceInfo.deviceId);
- },
- child: Text(mediaDeviceInfo.label),
- );
- },
- ).toList(),
- );
- }
- },
- );
- },
- ).then((deviceId) {
- log("onCameraSelected deviceId: $deviceId", TAG);
- if (deviceId != null) _callSession.switchCamera(deviceId: deviceId);
- });
- }
- }
-
- _toggleCamera() {
- if (!_isVideoCall()) return;
-
- setState(() {
- _isCameraEnabled = !_isCameraEnabled;
- _callSession.setVideoEnabled(_isCameraEnabled);
- });
- }
-
- _toggleScreenSharing() async {
- var foregroundServiceFuture = _enableScreenSharing
- ? startBackgroundExecution()
- : stopBackgroundExecution();
-
- var hasPermissions = await hasBackgroundExecutionPermissions();
-
- if (!hasPermissions) {
- await initForegroundService();
- }
-
- var desktopCapturerSource = _enableScreenSharing && isDesktop
- ? await showDialog(
- context: context,
- builder: (context) => ScreenSelectDialog(),
- )
- : null;
-
- foregroundServiceFuture.then((_) {
- _callSession
- .enableScreenSharing(_enableScreenSharing,
- desktopCapturerSource: desktopCapturerSource,
- useIOSBroadcasting: true,
- requestAudioForScreenSharing: true)
- .then((voidResult) {
- setState(() {
- _enableScreenSharing = !_enableScreenSharing;
- _isFrontCameraUsed = _enableScreenSharing;
- });
- });
- });
- }
-
- _switchPrimaryVideoFit() async {
- setState(() {
- primaryVideoFit =
- primaryVideoFit == RTCVideoViewObjectFit.RTCVideoViewObjectFitCover
- ? RTCVideoViewObjectFit.RTCVideoViewObjectFitContain
- : RTCVideoViewObjectFit.RTCVideoViewObjectFitCover;
- });
- }
-
- bool _isVideoEnabled() {
- return _isVideoCall() && _isCameraEnabled;
- }
-
- bool _isVideoCall() {
- return CallType.VIDEO_CALL == _callSession.callType;
- }
-
- _switchSpeaker() {
- if (kIsWeb || WebRTC.platformIsDesktop) {
- showDialog(
- context: context,
- builder: (BuildContext context) {
- return FutureBuilder>(
- future: _callSession.getAudioOutputs(),
- builder: (context, snapshot) {
- if (!snapshot.hasData || snapshot.data!.isEmpty) {
- return AlertDialog(
- content: const Text('No Audio output devices found'),
- actions: [
- TextButton(
- style: TextButton.styleFrom(
- textStyle: Theme.of(context).textTheme.labelLarge,
- ),
- child: const Text('Ok'),
- onPressed: () {
- Navigator.of(context).pop();
- },
- )
- ],
- );
- } else {
- return SimpleDialog(
- title: const Text('Select Audio output device'),
- children: snapshot.data?.map(
- (mediaDeviceInfo) {
- return SimpleDialogOption(
- onPressed: () {
- Navigator.pop(context, mediaDeviceInfo.deviceId);
- },
- child: Text(mediaDeviceInfo.label),
- );
- },
- ).toList(),
- );
- }
- },
- );
- },
- ).then((deviceId) {
- log("onAudioOutputSelected deviceId: $deviceId", TAG);
- if (deviceId != null) {
- setState(() {
- if (kIsWeb) {
- primaryRenderer?.value.audioOutput(deviceId);
- minorRenderers.forEach((userId, renderer) {
- renderer.audioOutput(deviceId);
- });
- } else {
- _callSession.selectAudioOutput(deviceId);
- }
- });
- }
- });
- } else {
- setState(() {
- _isSpeakerEnabled = !_isSpeakerEnabled;
- _callSession.enableSpeakerphone(_isSpeakerEnabled);
- });
- }
- }
-
- _switchAudioInput() {
- if (kIsWeb || WebRTC.platformIsDesktop) {
- showDialog(
- context: context,
- builder: (BuildContext context) {
- return FutureBuilder>(
- future: _callSession.getAudioInputs(),
- builder: (context, snapshot) {
- if (!snapshot.hasData || snapshot.data!.isEmpty) {
- return AlertDialog(
- content: const Text('No Audio input devices found'),
- actions: [
- TextButton(
- style: TextButton.styleFrom(
- textStyle: Theme.of(context).textTheme.labelLarge,
- ),
- child: const Text('Ok'),
- onPressed: () {
- Navigator.of(context).pop();
- },
- )
- ],
- );
- } else {
- return SimpleDialog(
- title: const Text('Select Audio input device'),
- children: snapshot.data?.map(
- (mediaDeviceInfo) {
- return SimpleDialogOption(
- onPressed: () {
- Navigator.pop(context, mediaDeviceInfo.deviceId);
- },
- child: Text(mediaDeviceInfo.label),
- );
- },
- ).toList(),
- );
- }
- },
- );
- },
- ).then((deviceId) {
- log("onAudioOutputSelected deviceId: $deviceId", TAG);
- if (deviceId != null) {
- setState(() {
- _callSession.selectAudioInput(deviceId);
- });
- }
- });
- }
- }
-
- void _initCustomMediaConfigs() {
- RTCMediaConfig mediaConfig = RTCMediaConfig.instance;
- mediaConfig.minHeight = HD_VIDEO.height;
- mediaConfig.minWidth = HD_VIDEO.width;
- }
-}
-
-enum LayoutMode { speaker, grid }
diff --git a/conf_call_sample/lib/src/managers/call_manager.dart b/conf_call_sample/lib/src/managers/call_manager.dart
new file mode 100644
index 0000000..e35a647
--- /dev/null
+++ b/conf_call_sample/lib/src/managers/call_manager.dart
@@ -0,0 +1,572 @@
+import 'dart:async';
+import 'dart:convert';
+
+import 'package:flutter/material.dart';
+import 'package:universal_io/io.dart';
+import 'package:uuid/uuid.dart';
+
+import 'package:connectycube_flutter_call_kit/connectycube_flutter_call_kit.dart';
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+import '../screens/conversation_call_screen.dart';
+import '../utils/consts.dart';
+import '../utils/pref_util.dart';
+import 'callkit_manager.dart';
+import 'push_notifications_manager.dart';
+
+const NO_ANSWER_TIMER_INTERVAL = 60;
+
+class CallManager {
+ static final String TAG = 'CallManager';
+ bool isInitialized = false;
+ SystemMessagesManager? _systemMessagesManager;
+ NewCallCallback? onReceiveNewCall;
+ CloseCall? onCloseCall;
+ RejectCallCallback? onReceiveRejectCall;
+ AcceptCallCallback? onReceiveAcceptCall;
+ CallActionCallback? onCallAccepted;
+ CallActionCallback? onCallRejected;
+ MuteCallCallback? onCallMuted;
+ UserNotAnswerCallback? onUserNotAnswerCallback;
+ String? _meetingId;
+ List? _participantIds;
+ int? _initiatorId;
+ Map _meetingsCalls = {};
+ InternalCallState? currentCallState;
+ Map Function()? getMediaState;
+ MediaStateUpdatedCallback? onParticipantMediaUpdated;
+
+ var _answerUserTimers = Map();
+
+ late BuildContext context;
+
+ CallManager._privateConstructor() {
+ RTCConfig.instance.statsReportsInterval = 200;
+ }
+
+ static final CallManager _instance = CallManager._privateConstructor();
+
+ static CallManager get instance => _instance;
+
+ init(BuildContext context) {
+ log('[init]', TAG);
+
+ if (isInitialized) return;
+
+ if (Platform.isAndroid || Platform.isIOS) {
+ PushNotificationsManager.instance.init();
+ CallKitManager.instance.init(
+ onCallAccepted: _onCallAccepted,
+ onCallEnded: _onCallEnded,
+ onMuteCall: _onMuteCall);
+ }
+
+ _initSignalingListener();
+
+ isInitialized = true;
+ }
+
+ parseCallMessage(CubeMessage cubeMessage) {
+ log("parseCallMessage cubeMessage= $cubeMessage");
+
+ if (cubeMessage.senderId == CubeChatConnection.instance.currentUser?.id)
+ return;
+
+ final properties = cubeMessage.properties;
+ var meetingId = properties[PARAM_MEETING_ID];
+ var callId = properties[PARAM_SESSION_ID]!;
+
+ if (properties.containsKey(SIGNAL_TYPE_START_CALL)) {
+ var participantIds = properties[PARAM_CALL_OPPONENTS]!
+ .split(',')
+ .map((id) => int.parse(id))
+ .toList();
+ var callType =
+ int.tryParse(properties[PARAM_CALL_TYPE]?.toString() ?? '') ??
+ CallType.VIDEO_CALL;
+ var callName = properties[PARAM_CALLER_NAME] ??
+ cubeMessage.senderId?.toString() ??
+ 'Unknown Caller';
+ if (_meetingId == null) {
+ currentCallState = InternalCallState.NEW;
+ onReceiveNewCall?.call(callId, meetingId!, cubeMessage.senderId!,
+ participantIds, callType, callName);
+ }
+ } else if (properties.containsKey(SIGNAL_TYPE_ACCEPT_CALL)) {
+ if (_meetingId == meetingId) {
+ handleAcceptCall(cubeMessage.senderId!);
+ }
+ } else if (properties.containsKey(SIGNAL_TYPE_REJECT_CALL)) {
+ bool isBusy = properties[PARAM_BUSY] == 'true';
+ if (_meetingId == meetingId) {
+ handleRejectCall(meetingId!, cubeMessage.senderId!, isBusy);
+ }
+ } else if (properties.containsKey(SIGNAL_TYPE_END_CALL)) {
+ processCallFinishedByParticipant(
+ cubeMessage.senderId!, callId, meetingId!);
+ } else if (properties.containsKey(SIGNAL_TYPE_UPDATE_MEDIA_STATE)) {
+ if (_meetingId == meetingId &&
+ properties.containsKey(PARAM_MEDIA_CONFIG)) {
+ var mediaConfig =
+ Map.from(jsonDecode(properties[PARAM_MEDIA_CONFIG]!));
+ onParticipantMediaUpdated?.call(cubeMessage.senderId!, mediaConfig);
+ }
+ } else if (properties.containsKey(SIGNAL_TYPE_REQUEST_MEDIA_STATE)) {
+ if (_meetingId == meetingId) {
+ var mediaConfig = getMediaState?.call();
+
+ if (mediaConfig != null) {
+ sendMediaUpdatedMessage(
+ callId, meetingId!, [cubeMessage.senderId!], mediaConfig);
+ }
+ }
+ }
+ }
+
+ startNewOutgoingCall(String meetingId, List participantIds,
+ int currentUserId, int callType, String callName, String? callPhoto) {
+ _initiatorId = currentUserId;
+ _participantIds = participantIds;
+ _meetingId = meetingId;
+ _meetingsCalls[_meetingId!] = Uuid().v4();
+ currentCallState = InternalCallState.NEW;
+ sendCallMessage(_meetingsCalls[_meetingId!]!, meetingId, participantIds,
+ callType, callName);
+ startNoAnswerTimers(participantIds);
+ _sendStartCallSignalForOffliners(_meetingsCalls[_meetingId!]!, meetingId,
+ callType, callName, callPhoto, currentUserId, participantIds.toSet());
+ }
+
+ reject(String callId, String meetingId, bool isBusy, int initiatorId,
+ bool fromCallKit) {
+ currentCallState = InternalCallState.REJECTED;
+ sendRejectMessage(callId, meetingId, isBusy, initiatorId);
+
+ if (!fromCallKit) {
+ CallKitManager.instance.processCallFinished(callId);
+ }
+
+ _clearCallData();
+ }
+
+ stopCall(CubeUser currentUser) {
+ currentCallState = InternalCallState.FINISHED;
+
+ _clearNoAnswerTimers();
+
+ if (_meetingId == null) return;
+
+ sendEndCallMessage(
+ _meetingsCalls[_meetingId!]!, _meetingId!, _participantIds!);
+ if (_initiatorId == currentUser.id) {
+ CallKitManager.instance.sendEndCallPushNotification(
+ _meetingsCalls[_meetingId!]!, _participantIds!);
+ }
+ CallKitManager.instance.processCallFinished(_meetingsCalls[_meetingId!]!);
+ _clearCallData();
+ }
+
+ processCallFinishedByParticipant(
+ int userId, String callId, String meetingId) {
+ if (_meetingId == null) {
+ currentCallState = InternalCallState.FINISHED;
+
+ onCloseCall?.call();
+ CallKitManager.instance.processCallFinished(callId);
+ } else if (_meetingId == meetingId) {
+ _clearCall(userId);
+ }
+ }
+
+ sendCallMessage(String callId, String meetingId, List participantIds,
+ int callType, String callName) {
+ List callMsgList =
+ buildCallMessages(callId, meetingId, participantIds);
+ callMsgList.forEach((callMsg) {
+ callMsg.properties[SIGNAL_TYPE_START_CALL] = '1';
+ callMsg.properties[PARAM_CALL_OPPONENTS] = participantIds.join(',');
+ callMsg.properties[PARAM_CALL_TYPE] = callType.toString();
+ callMsg.properties[PARAM_CALLER_NAME] = callName;
+ });
+ callMsgList
+ .forEach((msg) => sendSystemMessage(msg.recipientId!, msg.properties));
+ }
+
+ sendAcceptMessage(String callId, String meetingId, int participantId) {
+ List callMsgList =
+ buildCallMessages(callId, meetingId, [participantId]);
+ callMsgList.forEach((callMsg) {
+ callMsg.properties[SIGNAL_TYPE_ACCEPT_CALL] = '1';
+ });
+ callMsgList
+ .forEach((msg) => sendSystemMessage(msg.recipientId!, msg.properties));
+ }
+
+ sendRejectMessage(
+ String callId, String meetingId, bool isBusy, int participantId) {
+ List callMsgList =
+ buildCallMessages(callId, meetingId, [participantId]);
+ callMsgList.forEach((callMsg) {
+ callMsg.properties[SIGNAL_TYPE_REJECT_CALL] = '1';
+ callMsg.properties[PARAM_BUSY] = isBusy.toString();
+ });
+ callMsgList
+ .forEach((msg) => sendSystemMessage(msg.recipientId!, msg.properties));
+ }
+
+ sendEndCallMessage(
+ String callId, String meetingId, List participantIds) {
+ List callMsgList =
+ buildCallMessages(callId, meetingId, participantIds);
+ callMsgList.forEach((callMsg) {
+ callMsg.properties[SIGNAL_TYPE_END_CALL] = '1';
+ });
+ callMsgList
+ .forEach((msg) => sendSystemMessage(msg.recipientId!, msg.properties));
+ }
+
+ sendMediaUpdatedMessage(String callId, String meetingId,
+ List participantIds, Map mediaConfig) {
+ List callMsgList =
+ buildCallMessages(callId, meetingId, participantIds);
+ callMsgList.forEach((callMsg) {
+ callMsg.properties[SIGNAL_TYPE_UPDATE_MEDIA_STATE] = '1';
+ callMsg.properties[PARAM_MEDIA_CONFIG] = jsonEncode(mediaConfig);
+ });
+ callMsgList
+ .forEach((msg) => sendSystemMessage(msg.recipientId!, msg.properties));
+ }
+
+ sendRequestMediaConfigMessage(
+ String callId, String meetingId, List participantIds) {
+ List callMsgList =
+ buildCallMessages(callId, meetingId, participantIds);
+ callMsgList.forEach((callMsg) {
+ callMsg.properties[SIGNAL_TYPE_REQUEST_MEDIA_STATE] = '1';
+ });
+ callMsgList
+ .forEach((msg) => sendSystemMessage(msg.recipientId!, msg.properties));
+ }
+
+ muteMic(String meetingId, bool mute) {
+ CallKitManager.instance.muteMic(_meetingsCalls[meetingId]!, mute);
+ }
+
+ handleAcceptCall(int participantId) {
+ _clearNoAnswerTimers(id: participantId);
+ onReceiveAcceptCall?.call(participantId);
+ }
+
+ handleRejectCall(String meetingId, int participantId, isBusy) {
+ onReceiveRejectCall?.call(meetingId, participantId, isBusy);
+ _clearNoAnswerTimers(id: participantId);
+ _clearCall(participantId);
+ }
+
+ startNoAnswerTimers(participantIds) {
+ participantIds.forEach((userId) => {
+ _answerUserTimers[userId] = Timer(
+ Duration(seconds: NO_ANSWER_TIMER_INTERVAL),
+ () => noUserAnswer(userId))
+ });
+ }
+
+ noUserAnswer(int participantId) {
+ onUserNotAnswerCallback?.call(participantId);
+ _clearNoAnswerTimers(id: participantId);
+ sendEndCallMessage(
+ _meetingsCalls[_meetingId!]!, _meetingId!, [participantId]);
+ _clearCall(participantId);
+ }
+
+ _clearNoAnswerTimers({int id = 0}) {
+ if (id != 0) {
+ _answerUserTimers[id]?.cancel();
+ _answerUserTimers.remove(id);
+ } else {
+ _answerUserTimers.forEach((participantId, timer) => timer.cancel());
+ _answerUserTimers.clear();
+ }
+ }
+
+ _clearCallData() {
+ log('[_clearProperties]', TAG);
+
+ _meetingId = null;
+ _initiatorId = null;
+ _participantIds = null;
+ _meetingsCalls.clear();
+ }
+
+ _clearCall(int participantId) {
+ _participantIds?.remove(participantId);
+ if ((_participantIds?.isEmpty ?? false) || participantId == _initiatorId) {
+ if (_meetingId != null) {
+ CallKitManager.instance
+ .processCallFinished(_meetingsCalls[_meetingId!]!);
+ }
+
+ _clearCallData();
+ currentCallState = InternalCallState.FINISHED;
+
+ onCloseCall?.call();
+ }
+ }
+
+ _onCallAccepted(CallEvent callEvent) async {
+ log('[_onCallAccepted] _currentCallState: $currentCallState', TAG);
+
+ if (currentCallState == InternalCallState.ACCEPTED) return;
+
+ var savedUser = await SharedPrefs.getUser();
+ if (savedUser == null) return;
+
+ var meetingId = callEvent.userInfo?[PARAM_MEETING_ID];
+ if (meetingId == null) return;
+
+ CallManager.instance.startNewIncomingCall(
+ context,
+ savedUser,
+ callEvent.sessionId,
+ meetingId,
+ callEvent.callType,
+ callEvent.callerName,
+ callEvent.callerId,
+ callEvent.opponentsIds.toList(),
+ true,
+ cleanNavigation: false,
+ );
+ }
+
+ _onCallEnded(CallEvent callEvent) async {
+ log('[_onCallEnded] _currentCallState: $currentCallState', TAG);
+
+ if (currentCallState == InternalCallState.FINISHED ||
+ currentCallState == InternalCallState.REJECTED) return;
+
+ var savedUser = await SharedPrefs.getUser();
+ if (savedUser == null) return;
+
+ var meetingId = callEvent.userInfo?[PARAM_MEETING_ID];
+ if (meetingId == null) return;
+
+ if (currentCallState == InternalCallState.ACCEPTED) {
+ stopCall(savedUser);
+ } else {
+ reject(callEvent.sessionId, meetingId, false, callEvent.callerId, true);
+ onCallRejected?.call(meetingId);
+ }
+ }
+
+ _onMuteCall(bool mute, String callId) {
+ if (!_meetingsCalls.containsValue(callId)) return;
+
+ _meetingsCalls.forEach((key, value) {
+ if (value == callId) {
+ onCallMuted?.call(key, mute);
+ }
+ });
+ }
+
+ void _sendStartCallSignalForOffliners(
+ String sessionId,
+ String meetingId,
+ int callType,
+ String callName,
+ String? callPhoto,
+ int callerId,
+ Set opponentsIds,
+ ) {
+ CreateEventParams params = _getCallEventParameters(sessionId, meetingId,
+ callType, callName, callPhoto, callerId, opponentsIds);
+ params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_START_CALL;
+ params.parameters[PARAM_IOS_VOIP] = 1;
+ params.parameters[PARAM_EXPIRATION] = 0;
+
+ createEvent(params.getEventForRequest()).then((cubeEvent) {
+ log("Event for offliners created: $cubeEvent");
+ }).catchError((error) {
+ log("ERROR occurs during create event");
+ });
+ }
+
+ CreateEventParams _getCallEventParameters(
+ String sessionId,
+ String meetingId,
+ int callType,
+ String callName,
+ String? callPhoto,
+ int callerId,
+ Set opponentsIds,
+ ) {
+ CreateEventParams params = CreateEventParams();
+ params.parameters = {
+ 'message':
+ "Incoming ${callType == CallType.VIDEO_CALL ? "Video" : "Audio"} call",
+ PARAM_CALL_TYPE: callType,
+ PARAM_SESSION_ID: sessionId,
+ PARAM_CALLER_ID: callerId,
+ PARAM_CALLER_NAME: callName,
+ PARAM_CALL_OPPONENTS: opponentsIds.join(','),
+ PARAM_PHOTO_URL: callPhoto,
+ PARAM_USER_INFO: jsonEncode({
+ PARAM_MEETING_ID: meetingId,
+ }),
+ };
+
+ params.notificationType = NotificationType.PUSH;
+ params.environment = CubeEnvironment
+ .DEVELOPMENT; // TODO use `DEVELOPMENT` for testing purposes
+ // params.environment = kReleaseMode ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT; // TODO use real in your app
+ params.usersIds = opponentsIds.toList();
+
+ return params;
+ }
+
+ void _initSignalingListener() {
+ initSignalingListener() {
+ _systemMessagesManager =
+ CubeChatConnection.instance.systemMessagesManager;
+ _systemMessagesManager?.systemMessagesStream.listen(parseCallMessage);
+ }
+
+ if (CubeChatConnection.instance.currentUser != null &&
+ CubeChatConnection.instance.chatConnectionState ==
+ CubeChatConnectionState.Ready) {
+ initSignalingListener();
+ } else {
+ CubeChatConnection.instance.connectionStateStream.listen((state) {
+ if (state == CubeChatConnectionState.Ready) {
+ initSignalingListener();
+ }
+ });
+ }
+ }
+
+ startNewIncomingCall(
+ BuildContext context,
+ CubeUser currentUser,
+ String callId,
+ String meetingId,
+ int callType,
+ String callName,
+ int callerId,
+ List opponentsIds,
+ bool fromCallKit, {
+ bool cleanNavigation = true,
+ MediaStream? initialLocalMediaStream,
+ bool isFrontCameraUsed = true,
+ }) async {
+ currentCallState = InternalCallState.ACCEPTED;
+
+ var participants = Set.from([...opponentsIds, callerId]);
+ participants.removeWhere((userId) => userId == currentUser.id!);
+
+ setActiveCall(callId, meetingId, callerId, participants.toList());
+
+ sendAcceptMessage(callId, meetingId, callerId);
+
+ if (fromCallKit) {
+ onCallAccepted?.call(meetingId);
+ } else {
+ CallKitManager.instance.processCallStarted(callId);
+ }
+
+ ConferenceSession callSession = await ConferenceClient.instance
+ .createCallSession(currentUser.id!, callType: callType);
+
+ var route = MaterialPageRoute(
+ builder: (context) => ConversationCallScreen(
+ currentUser,
+ callSession,
+ meetingId,
+ opponentsIds,
+ true,
+ callName,
+ initialLocalMediaStream: initialLocalMediaStream,
+ isFrontCameraUsed: isFrontCameraUsed,
+ ),
+ );
+
+ if (cleanNavigation) {
+ Navigator.of(context).pushReplacement(route);
+ } else {
+ Navigator.of(context).push(route);
+ }
+ }
+
+ static Future startCallIfNeed(BuildContext context) async {
+ var savedUser = await SharedPrefs.getUser();
+ if (savedUser == null) return;
+
+ CallKitManager.instance.getCallToStart().then((callToStart) async {
+ if (callToStart != null && callToStart.userInfo != null) {
+ var meetingId = callToStart.userInfo![PARAM_MEETING_ID]!;
+
+ CallManager.instance.startNewIncomingCall(
+ context,
+ savedUser,
+ callToStart.sessionId,
+ meetingId,
+ callToStart.callType,
+ callToStart.callerName,
+ callToStart.callerId,
+ callToStart.opponentsIds.toList(),
+ true);
+ }
+ });
+ }
+
+ bool hasActiveCall() {
+ return _meetingId != null;
+ }
+
+ void setActiveCall(String callId, String meetingId, int initiatorId,
+ List participantIds) {
+ _meetingId = meetingId;
+ _meetingsCalls[meetingId] = callId;
+ _initiatorId = initiatorId;
+ _participantIds = participantIds;
+ }
+
+ void notifyParticipantsMediaUpdated(Map mediaConfig) {
+ if (_meetingId == null) return;
+
+ sendMediaUpdatedMessage(_meetingsCalls[_meetingId]!, _meetingId!,
+ _participantIds!, mediaConfig);
+ }
+
+ void requestParticipantsMediaConfig(List participants) {
+ if (_meetingId == null) return;
+ participants.removeWhere((userId) => userId == null);
+
+ if (participants.isEmpty) return;
+
+ sendRequestMediaConfigMessage(
+ _meetingsCalls[_meetingId]!, _meetingId!, List.from(participants));
+ }
+}
+
+List buildCallMessages(
+ String callId, String meetingId, List participantIds) {
+ return participantIds.map((userId) {
+ var msg = CubeMessage();
+ msg.recipientId = userId;
+ msg.properties = {PARAM_MEETING_ID: meetingId, PARAM_SESSION_ID: callId};
+ return msg;
+ }).toList();
+}
+
+enum InternalCallState { NEW, REJECTED, ACCEPTED, FINISHED }
+
+typedef void NewCallCallback(String callId, String meetingId, int initiatorId,
+ List participantIds, int callType, String callName);
+typedef void CloseCall();
+typedef void RejectCallCallback(
+ String meetingId, int participantId, bool isBusy);
+typedef void AcceptCallCallback(int participantId);
+typedef void CallActionCallback(String meetingId);
+typedef void UserNotAnswerCallback(int participantId);
+typedef void MuteCallCallback(String meetingId, bool isMuted);
+typedef void MediaStateUpdatedCallback(
+ int userId, Map mediaConfig);
diff --git a/conf_call_sample/lib/src/managers/callkit_manager.dart b/conf_call_sample/lib/src/managers/callkit_manager.dart
new file mode 100644
index 0000000..a3686be
--- /dev/null
+++ b/conf_call_sample/lib/src/managers/callkit_manager.dart
@@ -0,0 +1,200 @@
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
+import 'package:universal_io/io.dart';
+
+import 'package:connectycube_flutter_call_kit/connectycube_flutter_call_kit.dart';
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+import '../../main.dart';
+import '../utils/consts.dart';
+import 'call_manager.dart';
+
+class CallKitManager {
+ static CallKitManager get instance => _getInstance();
+ static CallKitManager? _instance;
+ static String TAG = "CallKitManager";
+
+ static CallKitManager _getInstance() {
+ return _instance ??= CallKitManager._internal();
+ }
+
+ factory CallKitManager() => _getInstance();
+
+ CallKitManager._internal();
+
+ late Function(CallEvent callEvent) onCallAccepted;
+ late Function(CallEvent CallEvent) onCallEnded;
+ late Function(bool mute, String uuid) onMuteCall;
+
+ init({
+ required onCallAccepted(CallEvent callEvent),
+ required onCallEnded(CallEvent callEvent),
+ required onMuteCall(bool mute, String uuid),
+ }) {
+ this.onCallAccepted = onCallAccepted;
+ this.onCallEnded = onCallEnded;
+ this.onMuteCall = onMuteCall;
+
+ ConnectycubeFlutterCallKit.instance.init(
+ onCallAccepted: _onCallAccepted,
+ onCallRejected: _onCallRejected,
+ icon: Platform.isAndroid ? 'default_avatar' : 'CallKitIcon',
+ color: '#07711e',
+ // ringtone:
+ // Platform.isAndroid ? 'custom_ringtone' : 'Resources/ringtones/custom_ringtone.caf'
+ );
+ ConnectycubeFlutterCallKit.onCallRejectedWhenTerminated =
+ onCallRejectedWhenTerminated;
+
+ if (Platform.isIOS) {
+ ConnectycubeFlutterCallKit.onCallMuted = _onCallMuted;
+ }
+ }
+
+ Future processCallFinished(String uuid) async {
+ if (Platform.isAndroid || Platform.isIOS) {
+ ConnectycubeFlutterCallKit.reportCallEnded(sessionId: uuid);
+ ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: false);
+ ConnectycubeFlutterCallKit.clearCallData(sessionId: uuid);
+ }
+ }
+
+ Future processCallStarted(String uuid) async {
+ if (Platform.isAndroid || Platform.isIOS) {
+ ConnectycubeFlutterCallKit.reportCallAccepted(sessionId: uuid);
+ ConnectycubeFlutterCallKit.setOnLockScreenVisibility(isVisible: true);
+ }
+ }
+
+ Future sendEndCallPushNotification(
+ String callId, List participants) async {
+ if (Platform.isAndroid || Platform.isIOS) {
+ sendPushAboutEndingCall(callId, participants);
+ }
+ }
+
+ Future _onCallMuted(bool mute, String callId) async {
+ onMuteCall.call(mute, callId);
+ }
+
+ Future _onCallAccepted(CallEvent callEvent) async {
+ onCallAccepted.call(callEvent);
+ }
+
+ Future _onCallRejected(CallEvent callEvent) async {
+ onCallEnded.call(callEvent);
+ }
+
+ void muteMic(String callId, bool mute) {
+ ConnectycubeFlutterCallKit.reportCallMuted(sessionId: callId, muted: mute);
+ }
+
+ Future getCallToStart() {
+ return ConnectycubeFlutterCallKit.getLastCallId().then((lastCallId) {
+ if (lastCallId == null) {
+ return null;
+ }
+
+ return ConnectycubeFlutterCallKit.getCallState(sessionId: lastCallId)
+ .then((state) {
+ if (state == CallState.ACCEPTED) {
+ return ConnectycubeFlutterCallKit.getCallData(sessionId: lastCallId)
+ .then((callData) {
+ if (callData == null) return null;
+
+ return CallEvent(
+ sessionId: callData[PARAM_SESSION_ID].toString(),
+ callType: int.parse(callData[PARAM_CALL_TYPE].toString()),
+ callerId: int.parse(callData[PARAM_CALLER_ID].toString()),
+ callerName: callData[PARAM_CALLER_NAME] as String,
+ opponentsIds: (callData[PARAM_CALL_OPPONENTS] as String)
+ .split(',')
+ .map(int.parse)
+ .toSet(),
+ userInfo: callData[PARAM_USER_INFO] != null
+ ? Map.from(
+ jsonDecode(callData[PARAM_USER_INFO]))
+ : null,
+ );
+ ;
+ });
+ }
+ return null;
+ });
+ });
+ }
+}
+
+@pragma('vm:entry-point')
+Future onCallRejectedWhenTerminated(CallEvent callEvent) async {
+ print(
+ '[PushNotificationsManager][onCallRejectedWhenTerminated] callEvent: $callEvent');
+
+ var meetingId = callEvent.userInfo?[PARAM_MEETING_ID];
+ if (meetingId == null) return;
+
+ initConnectycubeContextLess();
+
+ var callMsgList =
+ buildCallMessages(callEvent.sessionId, meetingId, [callEvent.callerId]);
+ callMsgList.forEach((callMsg) {
+ callMsg.properties[SIGNAL_TYPE_REJECT_CALL] = '1';
+ callMsg.properties[PARAM_BUSY] = 'false';
+ });
+
+ callMsgList
+ .forEach((msg) => sendSystemMessage(msg.recipientId!, msg.properties));
+
+ var sendRejectCallMessage = callMsgList.map((msg) {
+ return sendSystemMessage(msg.recipientId!, msg.properties);
+ }).toList();
+
+ var sendPushAboutReject = sendPushAboutRejectFromKilledState({
+ PARAM_CALL_TYPE: callEvent.callType,
+ PARAM_SESSION_ID: callEvent.sessionId,
+ PARAM_CALLER_ID: callEvent.callerId,
+ PARAM_CALLER_NAME: callEvent.callerName,
+ PARAM_CALL_OPPONENTS: callEvent.opponentsIds.join(','),
+ PARAM_USER_INFO: {PARAM_MEETING_ID: meetingId},
+ }, callEvent.callerId);
+
+ return Future.wait([...sendRejectCallMessage, sendPushAboutReject])
+ .then((result) {
+ return Future.value();
+ });
+}
+
+Future sendPushAboutRejectFromKilledState(
+ Map parameters,
+ int callerId,
+) {
+ CreateEventParams params = CreateEventParams();
+ params.parameters = parameters;
+ params.parameters[PARAM_MESSAGE] = "Reject call";
+ params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_REJECT_CALL;
+
+ params.notificationType = NotificationType.PUSH;
+ params.environment =
+ kReleaseMode ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;
+ params.usersIds = [callerId];
+
+ return createEvent(params.getEventForRequest());
+}
+
+Future sendPushAboutEndingCall(
+ String callId,
+ List participants,
+) {
+ CreateEventParams params = CreateEventParams();
+ params.parameters[PARAM_SESSION_ID] = callId;
+ params.parameters[PARAM_MESSAGE] = 'End call';
+ params.parameters[PARAM_SIGNAL_TYPE] = SIGNAL_TYPE_END_CALL;
+
+ params.notificationType = NotificationType.PUSH;
+ params.environment =
+ kReleaseMode ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT;
+ params.usersIds = participants;
+
+ return createEvent(params.getEventForRequest());
+}
diff --git a/conf_call_sample/lib/src/managers/push_notifications_manager.dart b/conf_call_sample/lib/src/managers/push_notifications_manager.dart
new file mode 100644
index 0000000..53b6b2a
--- /dev/null
+++ b/conf_call_sample/lib/src/managers/push_notifications_manager.dart
@@ -0,0 +1,123 @@
+import 'dart:convert';
+
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:package_info_plus/package_info_plus.dart';
+import 'package:universal_io/io.dart';
+
+import 'package:connectycube_flutter_call_kit/connectycube_flutter_call_kit.dart';
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+import '../utils/pref_util.dart';
+
+class PushNotificationsManager {
+ static const TAG = "PushNotificationsManager";
+
+ static PushNotificationsManager? _instance;
+
+ PushNotificationsManager._internal();
+
+ static PushNotificationsManager _getInstance() {
+ return _instance ??= PushNotificationsManager._internal();
+ }
+
+ factory PushNotificationsManager() => _getInstance();
+
+ BuildContext? applicationContext;
+
+ static PushNotificationsManager get instance => _getInstance();
+
+ init() async {
+ ConnectycubeFlutterCallKit.initEventsHandler();
+
+ ConnectycubeFlutterCallKit.onTokenRefreshed = (token) {
+ log('[onTokenRefresh] VoIP token: $token', TAG);
+ subscribe(token);
+ };
+
+ ConnectycubeFlutterCallKit.getToken().then((token) {
+ log('[getToken] VoIP token: $token', TAG);
+ if (token != null) {
+ subscribe(token);
+ }
+ });
+ }
+
+ subscribe(String token) async {
+ log('[subscribe] token: $token', PushNotificationsManager.TAG);
+
+ var savedToken = await SharedPrefs.getSubscriptionToken();
+ if (token == savedToken) {
+ log('[subscribe] skip subscription for same token',
+ PushNotificationsManager.TAG);
+ return;
+ }
+
+ CreateSubscriptionParameters parameters = CreateSubscriptionParameters();
+ parameters.pushToken = token;
+
+ parameters.environment = CubeEnvironment
+ .DEVELOPMENT; // TODO used `DEVELOPMENT` environment for testing purposes
+ // parameters.environment = kReleaseMode ? CubeEnvironment.PRODUCTION : CubeEnvironment.DEVELOPMENT; // TODO use actual environment in the real app
+
+ if (Platform.isAndroid) {
+ parameters.channel = NotificationsChannels.GCM;
+ parameters.platform = CubePlatform.ANDROID;
+ } else if (Platform.isIOS) {
+ parameters.channel = NotificationsChannels.APNS_VOIP;
+ parameters.platform = CubePlatform.IOS;
+ }
+
+ var deviceInfoPlugin = DeviceInfoPlugin();
+
+ var deviceId;
+
+ if (kIsWeb) {
+ var webBrowserInfo = await deviceInfoPlugin.webBrowserInfo;
+ deviceId = base64Encode(utf8.encode(webBrowserInfo.userAgent ?? ''));
+ } else if (Platform.isAndroid) {
+ var androidInfo = await deviceInfoPlugin.androidInfo;
+ deviceId = androidInfo.id;
+ } else if (Platform.isIOS) {
+ var iosInfo = await deviceInfoPlugin.iosInfo;
+ deviceId = iosInfo.identifierForVendor;
+ } else if (Platform.isMacOS) {
+ var macOsInfo = await deviceInfoPlugin.macOsInfo;
+ deviceId = macOsInfo.computerName;
+ }
+
+ parameters.udid = deviceId;
+
+ var packageInfo = await PackageInfo.fromPlatform();
+ parameters.bundleIdentifier = packageInfo.packageName;
+
+ createSubscription(parameters.getRequestParameters())
+ .then((cubeSubscriptions) {
+ log('[subscribe] subscription SUCCESS', PushNotificationsManager.TAG);
+ SharedPrefs.saveSubscriptionToken(token);
+ cubeSubscriptions.forEach((subscription) {
+ if (subscription.device!.clientIdentificationSequence == token) {
+ SharedPrefs.saveSubscriptionId(subscription.id!);
+ }
+ });
+ }).catchError((error) {
+ log('[subscribe] subscription ERROR: $error',
+ PushNotificationsManager.TAG);
+ });
+ }
+
+ Future unsubscribe() {
+ return SharedPrefs.getSubscriptionId().then((subscriptionId) async {
+ if (subscriptionId != 0) {
+ return deleteSubscription(subscriptionId).then((voidResult) {
+ SharedPrefs.saveSubscriptionId(0);
+ });
+ } else {
+ return Future.value();
+ }
+ }).catchError((onError) {
+ log('[unsubscribe] ERROR: $onError', PushNotificationsManager.TAG);
+ });
+ }
+}
diff --git a/conf_call_sample/lib/src/utils/speakers_manager.dart b/conf_call_sample/lib/src/managers/speakers_manager.dart
similarity index 100%
rename from conf_call_sample/lib/src/utils/speakers_manager.dart
rename to conf_call_sample/lib/src/managers/speakers_manager.dart
diff --git a/conf_call_sample/lib/src/screens/conversation_call_screen.dart b/conf_call_sample/lib/src/screens/conversation_call_screen.dart
new file mode 100644
index 0000000..bd3b05f
--- /dev/null
+++ b/conf_call_sample/lib/src/screens/conversation_call_screen.dart
@@ -0,0 +1,967 @@
+import 'dart:async';
+import 'dart:io';
+
+import 'package:assets_audio_player/assets_audio_player.dart';
+import 'package:conf_call_sample/src/utils/duration_timer.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+import '../managers/call_manager.dart';
+import '../utils/configs.dart';
+import '../utils/consts.dart';
+import '../utils/media_utils.dart';
+import '../utils/platform_utils.dart';
+import '../widgets/call_controls_widget.dart';
+import '../widgets/call_info_widget.dart';
+import '../widgets/grid_view_call_widget.dart';
+import '../widgets/private_call_widget.dart';
+import '../widgets/speaker_view_call_widget.dart';
+import 'select_opponents_screen.dart';
+
+class ConversationCallScreen extends StatefulWidget {
+ final CubeUser _currentUser;
+ final ConferenceSession _callSession;
+ final String _meetingId;
+ final List opponents;
+ final bool _isIncoming;
+ final String _callName;
+ final MediaStream? initialLocalMediaStream;
+ final bool isFrontCameraUsed;
+
+ @override
+ State createState() {
+ return _ConversationCallScreenState(
+ _currentUser,
+ _callSession,
+ _meetingId,
+ opponents,
+ _isIncoming,
+ _callName,
+ initialLocalMediaStream: initialLocalMediaStream,
+ isFrontCameraUsed: isFrontCameraUsed,
+ );
+ }
+
+ ConversationCallScreen(this._currentUser, this._callSession, this._meetingId,
+ this.opponents, this._isIncoming, this._callName,
+ {this.initialLocalMediaStream, this.isFrontCameraUsed = true});
+}
+
+class _ConversationCallScreenState extends State {
+ static const String TAG = "_ConversationCallScreenState";
+ static final LayoutMode DEFAULT_LAYOUT_MODE = LayoutMode.speaker;
+ final CubeUser _currentUser;
+ final ConferenceSession _callSession;
+ CallManager _callManager = CallManager.instance;
+ final String _callName;
+ final bool _isIncoming;
+ final String _meetingId;
+ final List _opponents;
+ final CubeStatsReportsManager _statsReportsManager =
+ CubeStatsReportsManager();
+ MediaStream? initialLocalMediaStream;
+
+ LayoutMode layoutMode = DEFAULT_LAYOUT_MODE;
+ String _callStatus = 'Waiting...';
+ bool _isCameraEnabled = true;
+ bool _isSpeakerEnabled = true;
+ bool _isMicMute = false;
+ bool _enableScreenSharing;
+ bool _isFrontCameraUsed = true;
+ RTCVideoViewObjectFit primaryVideoFit =
+ RTCVideoViewObjectFit.RTCVideoViewObjectFitCover;
+ final int currentUserId;
+ DurationTimer _callTimer = DurationTimer();
+ MapEntry? primaryRenderer;
+ Map minorRenderers = {};
+ Map> participantsMediaConfigs = {};
+ WidgetPosition _minorWidgetPosition = WidgetPosition.topRight;
+ AssetsAudioPlayer _ringtonePlayer = AssetsAudioPlayer.newPlayer();
+
+ _ConversationCallScreenState(this._currentUser, this._callSession,
+ this._meetingId, this._opponents, this._isIncoming, this._callName,
+ {this.initialLocalMediaStream, bool isFrontCameraUsed = true})
+ : _enableScreenSharing = !_callSession.startScreenSharing,
+ _isCameraEnabled = _callSession.callType == CallType.VIDEO_CALL,
+ currentUserId = _currentUser.id!,
+ _isFrontCameraUsed = isFrontCameraUsed {
+ if (_opponents.length == 1) {
+ layoutMode = LayoutMode.private;
+ }
+
+ if (initialLocalMediaStream != null) {
+ _isMicMute =
+ !(initialLocalMediaStream?.getAudioTracks().firstOrNull?.enabled ??
+ false);
+ _isCameraEnabled =
+ initialLocalMediaStream?.getVideoTracks().firstOrNull?.enabled ??
+ false;
+ }
+
+ participantsMediaConfigs[currentUserId] = {
+ PARAM_IS_MIC_ENABLED: !_isMicMute,
+ PARAM_IS_CAMERA_ENABLED: _isCameraEnabled
+ };
+ }
+
+ @override
+ void initState() {
+ super.initState();
+ _statsReportsManager.init(_callSession);
+
+ _callManager.onReceiveRejectCall = _onReceiveRejectCall;
+ _callManager.onReceiveAcceptCall = _onReceiveAcceptCall;
+ _callManager.onCloseCall = _onCloseCall;
+ _callManager.onCallMuted = _onCallMuted;
+ _callManager.getMediaState = _getMediaState;
+ _callManager.onParticipantMediaUpdated = _onParticipantMediaUpdated;
+
+ _callSession.onLocalStreamReceived = _addLocalMediaStream;
+ _callSession.onRemoteStreamTrackReceived = _addRemoteMediaStream;
+ _callSession.onSessionClosed = _onSessionClosed;
+ _callSession.onPublishersReceived = onPublishersReceived;
+ _callSession.onPublisherLeft = onPublisherLeft;
+ _callSession.onError = onError;
+ _callSession.onSubStreamChanged = onSubStreamChanged;
+ _callSession.onLayerChanged = onLayerChanged;
+
+ if (initialLocalMediaStream != null) {
+ _callSession.localStream = initialLocalMediaStream;
+ _addLocalMediaStream(initialLocalMediaStream!);
+ }
+
+ _callSession.joinDialog(_meetingId, ((publishers) {
+ log("join session= $publishers", TAG);
+
+ _callManager.requestParticipantsMediaConfig(publishers);
+
+ _callSession.setMaxBandwidth(0);
+
+ if (!_isIncoming) {
+ _callManager.startNewOutgoingCall(
+ _meetingId,
+ _opponents,
+ _callSession.currentUserId,
+ _callSession.callType,
+ _callName,
+ _currentUser.avatar);
+ _playDialing();
+ } else {
+ setState(() {
+ _callStatus = 'Connected';
+ _startCallTimer();
+ });
+ }
+ }), conferenceRole: ConferenceRole.PUBLISHER);
+ }
+
+ @override
+ void dispose() {
+ _statsReportsManager.dispose();
+
+ stopBackgroundExecution();
+
+ primaryRenderer?.value.srcObject = null;
+ primaryRenderer?.value.dispose();
+
+ minorRenderers.forEach((opponentId, renderer) {
+ log("[dispose] dispose renderer for $opponentId", TAG);
+ try {
+ renderer.srcObject = null;
+ renderer.dispose();
+ } catch (e) {
+ log('Error $e');
+ }
+ });
+
+ _playStoppingCall();
+
+ super.dispose();
+ }
+
+ void _onCloseCall() {
+ log("_onCloseCall", TAG);
+ _callSession.leave();
+ }
+
+ void _onReceiveRejectCall(String meetingId, int participantId, bool isBusy) {
+ log("_onReceiveRejectCall got reject from user $participantId", TAG);
+ }
+
+ void _onReceiveAcceptCall(int participantId) {
+ log('[_onReceiveAcceptCall] from user $participantId', TAG);
+
+ setState(() {
+ _callStatus = 'Connected';
+ _startCallTimer();
+ _stopDialing();
+ });
+ }
+
+ Future _addLocalMediaStream(MediaStream stream) async {
+ log("_addLocalMediaStream", TAG);
+
+ _addMediaStream(currentUserId, true, stream);
+ }
+
+ void _addRemoteMediaStream(session, int userId, MediaStream stream,
+ {String? trackId}) {
+ log("_addRemoteMediaStream for user $userId", TAG);
+
+ _addMediaStream(userId, false, stream, trackId: trackId);
+ }
+
+ void _removeMediaStream(callSession, int userId) {
+ log("_removeMediaStream for user $userId", TAG);
+ RTCVideoRenderer? videoRenderer = minorRenderers[userId];
+ if (videoRenderer != null) {
+ videoRenderer.srcObject = null;
+ videoRenderer.dispose();
+
+ setState(() {
+ minorRenderers.remove(userId);
+ });
+ } else if (primaryRenderer?.key == userId) {
+ var rendererToRemove = primaryRenderer?.value;
+
+ if (rendererToRemove != null) {
+ rendererToRemove.srcObject = null;
+ rendererToRemove.dispose();
+ }
+
+ if (minorRenderers.isNotEmpty) {
+ setState(() {
+ var userIdToRemoveRenderer = minorRenderers.keys.firstWhere(
+ (key) => key != currentUserId,
+ orElse: () => minorRenderers.keys.first);
+
+ primaryRenderer = MapEntry(userIdToRemoveRenderer,
+ minorRenderers.remove(userIdToRemoveRenderer)!);
+ chooseOpponentsStreamsQuality(_callSession, currentUserId,
+ {userIdToRemoveRenderer: StreamType.high});
+ });
+ }
+ }
+ }
+
+ void _closeSessionIfLast() {
+ log("[_closeSessionIfLast]", TAG);
+ if (_callSession.allActivePublishers.length < 1) {
+ log("[_closeSessionIfLast] 1", TAG);
+ _callSession.leave();
+ }
+ }
+
+ void _onSessionClosed(session) {
+ log("[_onSessionClosed]", TAG);
+ _statsReportsManager.dispose();
+ _callManager.stopCall(_currentUser);
+ _stopCallTimer();
+
+ log("[_onSessionClosed] 2", TAG);
+ var navigator = Navigator.of(context);
+ if (navigator.canPop()) {
+ log("[_onSessionClosed] navigator.canPop()", TAG);
+ Navigator.of(context).popUntil((route) {
+ return route.isFirst;
+ });
+ } else {
+ log("[_onSessionClosed] !navigator.canPop()", TAG);
+ navigator.pushReplacement(
+ MaterialPageRoute(
+ builder: (context) => SelectOpponentsScreen(_currentUser),
+ ),
+ );
+ }
+ }
+
+ void onPublishersReceived(publishers) {
+ log("onPublishersReceived", TAG);
+ handlePublisherReceived(publishers);
+ }
+
+ void onPublisherLeft(publisher) {
+ log("onPublisherLeft $publisher", TAG);
+ _removeMediaStream(_callSession, publisher);
+ _closeSessionIfLast();
+ }
+
+ void onError(ex) {
+ log("onError $ex", TAG);
+ }
+
+ void onSubStreamChanged(int userId, StreamType streamType) {
+ log("onSubStreamChanged userId: $userId, streamType: $streamType", TAG);
+ }
+
+ void onLayerChanged(int userId, int layer) {
+ log("onLayerChanged userId: $userId, layer: $layer", TAG);
+ }
+
+ Future _addMediaStream(
+ int userId, bool isLocalStream, MediaStream stream,
+ {String? trackId}) async {
+ log('[_addMediaStream] userId: $userId, isLocalStream: $isLocalStream',
+ TAG);
+
+ if (primaryRenderer == null) {
+ primaryRenderer = MapEntry(userId, RTCVideoRenderer());
+ await primaryRenderer!.value.initialize();
+
+ setState(() {
+ _setSourceForRenderer(primaryRenderer!.value, stream, isLocalStream,
+ trackId: trackId);
+
+ chooseOpponentsStreamsQuality(_callSession, currentUserId, {
+ userId: StreamType.high,
+ });
+ });
+
+ return;
+ }
+
+ if (primaryRenderer?.key == userId) {
+ _setSourceForRenderer(primaryRenderer!.value, stream, isLocalStream,
+ trackId: trackId);
+
+ chooseOpponentsStreamsQuality(_callSession, currentUserId, {
+ userId: StreamType.high,
+ });
+
+ return;
+ }
+
+ if (minorRenderers[userId] == null) {
+ minorRenderers[userId] = RTCVideoRenderer();
+ await minorRenderers[userId]?.initialize();
+ }
+
+ setState(() {
+ _setSourceForRenderer(minorRenderers[userId]!, stream, isLocalStream,
+ trackId: trackId);
+
+ if (primaryRenderer?.key == currentUserId ||
+ primaryRenderer?.key == userId ||
+ ((primaryRenderer?.value.srcObject?.getVideoTracks().isEmpty ??
+ false) &&
+ stream.getVideoTracks().isNotEmpty)) {
+ _updatePrimaryUser(userId, true);
+ }
+ });
+ }
+
+ _setSourceForRenderer(
+ RTCVideoRenderer renderer, MediaStream stream, bool isLocalStream,
+ {String? trackId}) {
+ isLocalStream || kIsWeb
+ ? renderer.srcObject = stream
+ : renderer.setSrcObject(stream: stream, trackId: trackId);
+ }
+
+ void handlePublisherReceived(List publishers) {
+ if (!_isIncoming) {
+ publishers.forEach((id) {
+ if (id != null) {
+ _callManager.handleAcceptCall(id);
+ }
+ });
+ }
+
+ if (publishers.isNotEmpty) {
+ _callManager.requestParticipantsMediaConfig(publishers);
+ }
+ }
+
+ void _updatePrimaryUser(int userId, bool force) {
+ log("[_updatePrimaryUser] userId: $userId, force: $force", TAG);
+
+ if (layoutMode == LayoutMode.grid) return;
+
+ log("[_updatePrimaryUser] 2", TAG);
+ updatePrimaryUser(
+ userId,
+ force,
+ currentUserId,
+ primaryRenderer,
+ minorRenderers,
+ participantsMediaConfigs,
+ onRenderersUpdated: _updateRenderers,
+ );
+ log("[_updatePrimaryUser] 3", TAG);
+ }
+
+ _updateRenderers(MapEntry? updatedPrimaryRenderer,
+ Map updatedMinorRenderers) {
+ if (updatedPrimaryRenderer?.key != primaryRenderer?.key) {
+ chooseOpponentsStreamsQuality(_callSession, currentUserId, {
+ if (updatedPrimaryRenderer?.key != null)
+ updatedPrimaryRenderer!.key: StreamType.high,
+ if (primaryRenderer?.key != null) primaryRenderer!.key: StreamType.low,
+ });
+ }
+
+ primaryRenderer = updatedPrimaryRenderer;
+ minorRenderers = updatedMinorRenderers;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return WillPopScope(
+ onWillPop: () => _onBackPressed(context),
+ child: Stack(
+ children: [
+ Scaffold(
+ backgroundColor: Colors.green.shade100,
+ body: _isVideoTracksPresent()
+ ? OrientationBuilder(
+ builder: (context, orientation) {
+ return layoutMode == LayoutMode.private
+ ? _buildPrivateCallLayout(orientation)
+ : layoutMode == LayoutMode.speaker
+ ? _buildSpeakerModLayout(orientation)
+ : _buildGridModLayout(orientation);
+ },
+ )
+ : _buildAudioCallLayout(),
+ ),
+ Align(
+ alignment: Alignment.bottomCenter,
+ child: _getActionsPanel(),
+ ),
+ Visibility(
+ visible:
+ layoutMode != LayoutMode.private && _isVideoTracksPresent(),
+ child: OrientationBuilder(
+ builder: (context, orientation) {
+ return Align(
+ alignment: Alignment.topLeft,
+ child: Container(
+ margin: orientation == Orientation.portrait
+ ? EdgeInsets.only(top: 40, left: 20)
+ : EdgeInsets.only(left: 40, top: 20),
+ child: FloatingActionButton(
+ elevation: 0,
+ heroTag: "ToggleScreenMode",
+ child: Icon(
+ layoutMode == LayoutMode.speaker
+ ? Icons.grid_view_rounded
+ : Icons.view_sidebar_rounded,
+ color: Colors.white,
+ ),
+ onPressed: () => _switchLayoutMode(),
+ backgroundColor: Colors.black38,
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ _switchLayoutMode() {
+ setState(() {
+ layoutMode = layoutMode == LayoutMode.speaker
+ ? LayoutMode.grid
+ : LayoutMode.speaker;
+
+ var config;
+ if (layoutMode == LayoutMode.grid) {
+ config = Map.fromEntries(minorRenderers
+ .map((key, value) => MapEntry(key, StreamType.medium))
+ .entries);
+ config.addEntries([MapEntry(primaryRenderer!.key, StreamType.medium)]);
+ } else {
+ config = Map.fromEntries(minorRenderers
+ .map((key, value) => MapEntry(key, StreamType.low))
+ .entries);
+ config.addEntries([MapEntry(primaryRenderer!.key, StreamType.high)]);
+ }
+
+ chooseOpponentsStreamsQuality(_callSession, currentUserId, config);
+ });
+ }
+
+ Widget _buildPrivateCallLayout(Orientation orientation) {
+ return PrivateCallLayout(
+ currentUserId: currentUserId,
+ primaryRenderer: primaryRenderer,
+ primaryVideoFit: primaryVideoFit,
+ minorRenderers: minorRenderers,
+ callName: _getCallName(),
+ callStatus: _callStatus,
+ callTimer: _callTimer,
+ minorWidgetInitialPosition: _minorWidgetPosition,
+ isFrontCameraUsed: _isFrontCameraUsed,
+ isScreenSharingEnabled: !_enableScreenSharing,
+ participantsMediaConfigs: participantsMediaConfigs,
+ onMinorVideoPositionChanged: (newPosition) {
+ _minorWidgetPosition = newPosition;
+ },
+ onPrimaryVideoFitChanged: (newObjectFit) {
+ primaryVideoFit = newObjectFit;
+ },
+ onRenderersChanged: _updateRenderers,
+ );
+ }
+
+ Widget _buildSpeakerModLayout(Orientation orientation) {
+ return SpeakerViewLayout(
+ currentUserId: currentUserId,
+ participants: users,
+ primaryRenderer: primaryRenderer,
+ primaryVideoFit: primaryVideoFit,
+ minorRenderers: minorRenderers,
+ callName: _getCallName(),
+ callStatus: _callStatus,
+ callTimer: _callTimer,
+ isFrontCameraUsed: _isFrontCameraUsed,
+ isScreenSharingEnabled: !_enableScreenSharing,
+ participantsMediaConfigs: participantsMediaConfigs,
+ onPrimaryVideoFitChanged: (newObjectFit) {
+ primaryVideoFit = newObjectFit;
+ },
+ onRenderersChanged: _updateRenderers,
+ statsReportsManager: _statsReportsManager,
+ );
+ }
+
+ Widget _buildGridModLayout(Orientation orientation) {
+ return GridViewLayout(
+ currentUserId: currentUserId,
+ participants: users,
+ primaryRenderer: primaryRenderer,
+ minorRenderers: minorRenderers,
+ isFrontCameraUsed: _isFrontCameraUsed,
+ isScreenSharingEnabled: !_enableScreenSharing,
+ participantsMediaConfigs: participantsMediaConfigs,
+ onRenderersChanged: _updateRenderers,
+ statsReportsManager: _statsReportsManager,
+ );
+ }
+
+ Widget _getActionsPanel() {
+ return CallControls(
+ isMicMuted: _isMicMute,
+ onMute: _muteMic,
+ isCameraButtonVisible: _enableScreenSharing,
+ isCameraEnabled: _isVideoEnabled(),
+ onToggleCamera: _toggleCamera,
+ isScreenSharingButtonVisible: _isLocalVideoPresented(),
+ isScreenSharingEnabled: !_enableScreenSharing,
+ onToggleScreenSharing: _toggleScreenSharing,
+ isSpeakerEnabled: _isSpeakerEnabled,
+ onSwitchSpeaker: _switchSpeaker,
+ onSwitchAudioInput: _switchAudioInput,
+ isSwitchCameraButtonVisible:
+ _isLocalVideoPresented() && _enableScreenSharing,
+ onSwitchCamera: _switchCamera,
+ onEndCall: _endCall,
+ );
+ }
+
+ _endCall() {
+ _callManager.stopCall(_currentUser);
+ _callSession.leave();
+ _stopCallTimer();
+ }
+
+ Future _onBackPressed(BuildContext context) {
+ return Future.value(false);
+ }
+
+ _muteMic() {
+ setState(() {
+ _isMicMute = !_isMicMute;
+ _callSession.setMicrophoneMute(_isMicMute);
+ _callManager.muteMic(_meetingId, _isMicMute);
+ notifyParticipantsMediaUpdated();
+ });
+ }
+
+ _switchCamera() {
+ if (!_isVideoEnabled()) return;
+
+ if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
+ _callSession.switchCamera().then((isFrontCameraUsed) {
+ setState(() {
+ _isFrontCameraUsed = isFrontCameraUsed;
+ });
+ });
+ } else {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return FutureBuilder>(
+ future: _callSession.getCameras(),
+ builder: (context, snapshot) {
+ if (!snapshot.hasData || snapshot.data!.isEmpty) {
+ return AlertDialog(
+ content: const Text('No cameras found'),
+ actions: [
+ TextButton(
+ style: TextButton.styleFrom(
+ textStyle: Theme.of(context).textTheme.labelLarge,
+ ),
+ child: const Text('Ok'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ )
+ ],
+ );
+ } else {
+ return SimpleDialog(
+ title: const Text('Select camera'),
+ children: snapshot.data?.map(
+ (mediaDeviceInfo) {
+ return SimpleDialogOption(
+ onPressed: () {
+ Navigator.pop(context, mediaDeviceInfo.deviceId);
+ },
+ child: Text(mediaDeviceInfo.label),
+ );
+ },
+ ).toList(),
+ );
+ }
+ },
+ );
+ },
+ ).then((deviceId) {
+ log("onCameraSelected deviceId: $deviceId", TAG);
+ if (deviceId != null) _callSession.switchCamera(deviceId: deviceId);
+ });
+ }
+ }
+
+ _toggleCamera() {
+ if (!_isVideoCall()) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ content: const Text(
+ 'Are you sure you want to start the sharing of your video?'),
+ actions: [
+ TextButton(
+ style: TextButton.styleFrom(
+ textStyle: Theme.of(context).textTheme.labelLarge,
+ ),
+ child: const Text('No'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ TextButton(
+ style: TextButton.styleFrom(
+ textStyle: Theme.of(context).textTheme.labelLarge,
+ ),
+ child: const Text('Yes'),
+ onPressed: () {
+ _addVideoTrack();
+ Navigator.of(context).pop();
+ },
+ ),
+ ],
+ );
+ });
+ } else {
+ setState(() {
+ _isCameraEnabled = !_isCameraEnabled;
+ _callSession.setVideoEnabled(_isCameraEnabled);
+ notifyParticipantsMediaUpdated();
+ });
+ }
+ }
+
+ _addVideoTrack() {
+ navigator.mediaDevices
+ .getUserMedia({'video': getVideoConfig()}).then((newMediaStream) {
+ if (newMediaStream.getVideoTracks().isNotEmpty) {
+ _callSession
+ .addMediaTrack(newMediaStream.getVideoTracks().first)
+ .whenComplete(() {
+ log('The track added successfully', TAG);
+ setState(() {
+ _isCameraEnabled = true;
+ _callSession.callType = CallType.VIDEO_CALL;
+ notifyParticipantsMediaUpdated();
+ });
+ });
+ }
+ });
+ }
+
+ _toggleScreenSharing() async {
+ var foregroundServiceFuture = _enableScreenSharing
+ ? startBackgroundExecution()
+ : stopBackgroundExecution();
+
+ var hasPermissions = await hasBackgroundExecutionPermissions();
+
+ if (!hasPermissions) {
+ await initForegroundService();
+ }
+
+ var desktopCapturerSource = _enableScreenSharing && isDesktop
+ ? await showDialog(
+ context: context,
+ builder: (context) => ScreenSelectDialog(),
+ )
+ : null;
+
+ foregroundServiceFuture.then((_) {
+ _callSession
+ .enableScreenSharing(_enableScreenSharing,
+ desktopCapturerSource: desktopCapturerSource,
+ useIOSBroadcasting: true,
+ requestAudioForScreenSharing: true)
+ .then((voidResult) {
+ setState(() {
+ _enableScreenSharing = !_enableScreenSharing;
+ _isFrontCameraUsed = _enableScreenSharing;
+ });
+ });
+ });
+ }
+
+ bool _isVideoEnabled() {
+ return _isVideoCall() && _isCameraEnabled;
+ }
+
+ bool _isVideoCall() {
+ return CallType.VIDEO_CALL == _callSession.callType;
+ }
+
+ _switchSpeaker() {
+ if (kIsWeb || WebRTC.platformIsDesktop) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return FutureBuilder>(
+ future: _callSession.getAudioOutputs(),
+ builder: (context, snapshot) {
+ if (!snapshot.hasData || snapshot.data!.isEmpty) {
+ return AlertDialog(
+ content: const Text('No Audio output devices found'),
+ actions: [
+ TextButton(
+ style: TextButton.styleFrom(
+ textStyle: Theme.of(context).textTheme.labelLarge,
+ ),
+ child: const Text('Ok'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ )
+ ],
+ );
+ } else {
+ return SimpleDialog(
+ title: const Text('Select Audio output device'),
+ children: snapshot.data?.map(
+ (mediaDeviceInfo) {
+ return SimpleDialogOption(
+ onPressed: () {
+ Navigator.pop(context, mediaDeviceInfo.deviceId);
+ },
+ child: Text(mediaDeviceInfo.label),
+ );
+ },
+ ).toList(),
+ );
+ }
+ },
+ );
+ },
+ ).then((deviceId) {
+ log("onAudioOutputSelected deviceId: $deviceId", TAG);
+ if (deviceId != null) {
+ setState(() {
+ if (kIsWeb) {
+ primaryRenderer?.value.audioOutput(deviceId);
+ minorRenderers.forEach((userId, renderer) {
+ renderer.audioOutput(deviceId);
+ });
+ } else {
+ _callSession.selectAudioOutput(deviceId);
+ }
+ });
+ }
+ });
+ } else {
+ setState(() {
+ _isSpeakerEnabled = !_isSpeakerEnabled;
+ _callSession.enableSpeakerphone(_isSpeakerEnabled);
+ });
+ }
+ }
+
+ _switchAudioInput() {
+ if (kIsWeb || WebRTC.platformIsDesktop) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return FutureBuilder>(
+ future: _callSession.getAudioInputs(),
+ builder: (context, snapshot) {
+ if (!snapshot.hasData || snapshot.data!.isEmpty) {
+ return AlertDialog(
+ content: const Text('No Audio input devices found'),
+ actions: [
+ TextButton(
+ style: TextButton.styleFrom(
+ textStyle: Theme.of(context).textTheme.labelLarge,
+ ),
+ child: const Text('Ok'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ )
+ ],
+ );
+ } else {
+ return SimpleDialog(
+ title: const Text('Select Audio input device'),
+ children: snapshot.data?.map(
+ (mediaDeviceInfo) {
+ return SimpleDialogOption(
+ onPressed: () {
+ Navigator.pop(context, mediaDeviceInfo.deviceId);
+ },
+ child: Text(mediaDeviceInfo.label),
+ );
+ },
+ ).toList(),
+ );
+ }
+ },
+ );
+ },
+ ).then((deviceId) {
+ log("onAudioOutputSelected deviceId: $deviceId", TAG);
+ if (deviceId != null) {
+ setState(() {
+ _callSession.selectAudioInput(deviceId);
+ });
+ }
+ });
+ }
+ }
+
+ bool _isVideoTracksPresent() {
+ var hasMinorVideo = false;
+ minorRenderers.forEach((key, value) {
+ if (canShowVideo(key, value.srcObject, participantsMediaConfigs)) {
+ hasMinorVideo = true;
+ }
+ });
+
+ return (primaryRenderer != null &&
+ canShowVideo(primaryRenderer?.key, primaryRenderer?.value.srcObject,
+ participantsMediaConfigs)) ||
+ hasMinorVideo;
+ }
+
+ bool _isLocalVideoPresented() {
+ if (primaryRenderer?.key == currentUserId) {
+ return primaryRenderer?.value.srcObject?.getVideoTracks().isNotEmpty ??
+ false;
+ }
+
+ var isLocalVideoPresented = false;
+
+ minorRenderers.forEach((userId, renderer) {
+ if (userId == currentUserId &&
+ (renderer.srcObject?.getVideoTracks().isNotEmpty ?? false)) {
+ isLocalVideoPresented = true;
+ }
+ });
+
+ return isLocalVideoPresented;
+ }
+
+ Widget _buildAudioCallLayout() {
+ return Align(
+ alignment: Alignment.topCenter,
+ child: Container(
+ margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top + 48),
+ child: CallInfo(_getCallName(), _callStatus, _callTimer)),
+ );
+ }
+
+ String _getCallName() {
+ if (_isIncoming) return _callName;
+
+ if (_opponents.length > 1) return 'Group call';
+
+ var opponent = users.firstWhere(
+ (savedUser) => savedUser.id == _opponents.first,
+ orElse: () => CubeUser(fullName: 'Unknown user'));
+
+ return opponent.fullName ?? 'Unknown user';
+ }
+
+ _startCallTimer() {
+ _callTimer.start();
+ }
+
+ _stopCallTimer() {
+ _callTimer.stop();
+ }
+
+ void _onCallMuted(String meetingId, bool isMuted) {
+ if (meetingId == _meetingId) {
+ setState(() {
+ _isMicMute = isMuted;
+ _callSession.setMicrophoneMute(isMuted);
+ });
+ }
+ }
+
+ Map _getMediaState() {
+ return {
+ PARAM_IS_MIC_ENABLED: !_isMicMute,
+ PARAM_IS_CAMERA_ENABLED: _isCameraEnabled
+ };
+ }
+
+ void _onParticipantMediaUpdated(int userId, Map mediaConfig) {
+ setState(() {
+ participantsMediaConfigs[userId] = mediaConfig;
+ });
+ }
+
+ void notifyParticipantsMediaUpdated() {
+ participantsMediaConfigs[currentUserId] = {
+ PARAM_IS_MIC_ENABLED: !_isMicMute,
+ PARAM_IS_CAMERA_ENABLED: _isCameraEnabled
+ };
+
+ _callManager.notifyParticipantsMediaUpdated({
+ PARAM_IS_MIC_ENABLED: !_isMicMute,
+ PARAM_IS_CAMERA_ENABLED: _isCameraEnabled
+ });
+ }
+
+ void _playDialing() {
+ _ringtonePlayer.open(Audio("assets/audio/dialing.mp3"),
+ loopMode: LoopMode.single);
+ }
+
+ void _stopDialing() {
+ _ringtonePlayer.stop();
+ }
+
+ void _playStoppingCall() {
+ _ringtonePlayer.open(Audio("assets/audio/end_call.mp3"),
+ loopMode: LoopMode.none);
+ }
+}
+
+enum LayoutMode { speaker, grid, private }
diff --git a/conf_call_sample/lib/src/screens/incoming_call_screen.dart b/conf_call_sample/lib/src/screens/incoming_call_screen.dart
new file mode 100644
index 0000000..7e6a4aa
--- /dev/null
+++ b/conf_call_sample/lib/src/screens/incoming_call_screen.dart
@@ -0,0 +1,500 @@
+import 'package:assets_audio_player/assets_audio_player.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:universal_io/io.dart';
+
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+import '../managers/call_manager.dart';
+
+class IncomingCallScreen extends StatefulWidget {
+ static const String TAG = "IncomingCallScreen";
+ final CubeUser _currentUser;
+ final String _callId;
+ final String _meetingId;
+ final int _initiatorId;
+ final List _participantIds;
+ final int _callType;
+ final String _callName;
+
+ IncomingCallScreen(this._currentUser, this._callId, this._meetingId,
+ this._initiatorId, this._participantIds, this._callType, this._callName);
+
+ @override
+ State createState() {
+ return _IncomingCallScreenState(_currentUser, _callId, _meetingId,
+ _initiatorId, _participantIds, _callType, _callName);
+ }
+}
+
+class _IncomingCallScreenState extends State {
+ static const String TAG = "_IncomingCallScreenState";
+ final CallManager _callManager = CallManager.instance;
+ final CubeUser _currentUser;
+ final String _callId;
+ final String _meetingId;
+ final int _initiatorId;
+ final List _participantIds;
+ final int _callType;
+ final String _callName;
+ bool _isFrontCameraSelected = true;
+ bool _isMicMute = false;
+ MediaStream? _localMediaStream;
+ RTCVideoRenderer? _localVideoRenderer;
+ AssetsAudioPlayer _ringtonePlayer = AssetsAudioPlayer.newPlayer();
+
+ _IncomingCallScreenState(this._currentUser, this._callId, this._meetingId,
+ this._initiatorId, this._participantIds, this._callType, this._callName);
+
+ @override
+ void initState() {
+ super.initState();
+ log('[initState]', TAG);
+
+ _callManager.onCloseCall = _onCallClosed;
+ _callManager.onCallAccepted = _onCallAccepted;
+ _callManager.onCallRejected = _onCallRejected;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ log('[build]', TAG);
+ if (_callManager.currentCallState != InternalCallState.NEW) {
+ closeScreen();
+ return SizedBox.shrink();
+ }
+
+ _playRingtone();
+
+ return WillPopScope(
+ onWillPop: () => _onBackPressed(context),
+ child: Scaffold(
+ backgroundColor: Colors.green.shade100,
+ body: FutureBuilder(
+ future: _getLocalMediaStream(),
+ builder: (context, snapshot) {
+ return Stack(
+ children: [
+ if (snapshot.hasData)
+ FutureBuilder(
+ future: getVideoRenderer(snapshot.data),
+ builder: (BuildContext context,
+ AsyncSnapshot snapshot2) {
+ if (!snapshot2.hasData) {
+ return SizedBox.shrink();
+ }
+
+ return RTCVideoView(
+ snapshot2.data,
+ objectFit:
+ RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
+ mirror: _isFrontCameraSelected,
+ );
+ },
+ ),
+ Container(
+ margin: EdgeInsets.only(
+ top: MediaQuery.of(context).padding.top + 80),
+ child: Align(
+ alignment: Alignment.topCenter,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text(_callName,
+ style: TextStyle(
+ color: Colors.white,
+ fontSize: 24,
+ shadows: [
+ Shadow(
+ color: Colors.grey.shade900,
+ offset: Offset(2, 1),
+ blurRadius: 12,
+ ),
+ ],
+ )),
+ ),
+ Padding(
+ padding: EdgeInsets.all(8),
+ child: Text(_getCallTitle(),
+ style: TextStyle(
+ fontSize: 18,
+ color: Colors.white,
+ shadows: [
+ Shadow(
+ color: Colors.grey.shade900,
+ offset: Offset(2, 1),
+ blurRadius: 12,
+ ),
+ ],
+ )),
+ ),
+ Expanded(
+ child: SizedBox(),
+ flex: 1,
+ ),
+ Visibility(
+ visible: _callType == CallType.VIDEO_CALL,
+ child: Padding(
+ padding: EdgeInsets.only(
+ bottom:
+ MediaQuery.of(context).padding.bottom +
+ 120),
+ child: Row(
+ // crossAxisAlignment: CrossAxisAlignment.center,
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Padding(
+ padding:
+ EdgeInsets.symmetric(horizontal: 16),
+ child: FloatingActionButton(
+ elevation: 0,
+ heroTag: "ToggleCamera",
+ child: Icon(
+ isVideoEnabledInStream(
+ _localMediaStream)
+ ? Icons.videocam
+ : Icons.videocam_off,
+ color: isVideoEnabledInStream(
+ _localMediaStream)
+ ? Colors.grey
+ : Colors.white,
+ ),
+ onPressed: () => _toggleCamera(),
+ backgroundColor: isVideoEnabledInStream(
+ _localMediaStream)
+ ? Colors.white
+ : Colors.grey,
+ ),
+ ),
+ Padding(
+ padding:
+ EdgeInsets.symmetric(horizontal: 16),
+ child: FloatingActionButton(
+ elevation: 0,
+ heroTag: "Mute",
+ child: Icon(
+ _isMicMute ? Icons.mic_off : Icons.mic,
+ color: _isMicMute
+ ? Colors.white
+ : Colors.grey,
+ ),
+ onPressed: () => _muteMic(),
+ backgroundColor: _isMicMute
+ ? Colors.grey
+ : Colors.white,
+ ),
+ ),
+ Padding(
+ padding:
+ EdgeInsets.symmetric(horizontal: 16),
+ child: FloatingActionButton(
+ elevation: 0,
+ heroTag: "SwitchCamera",
+ child: Icon(
+ Icons.cameraswitch,
+ color: isVideoEnabledInStream(
+ _localMediaStream)
+ ? Colors.white
+ : Colors.grey,
+ ),
+ onPressed: () => _switchCamera(),
+ backgroundColor: Colors.black38,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.only(
+ bottom:
+ MediaQuery.of(context).padding.bottom + 80),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: EdgeInsets.only(right: 36),
+ child: FloatingActionButton(
+ heroTag: "RejectCall",
+ child: Icon(
+ Icons.call_end,
+ color: Colors.white,
+ ),
+ backgroundColor: Colors.red,
+ onPressed: () => _rejectCall(),
+ ),
+ ),
+ Padding(
+ padding: EdgeInsets.only(left: 36),
+ child: FloatingActionButton(
+ heroTag: "AcceptCall",
+ child: Icon(
+ _callType == CallType.VIDEO_CALL
+ ? Icons.videocam
+ : Icons.call,
+ color: Colors.white,
+ ),
+ backgroundColor: Colors.green,
+ onPressed: () => _acceptCall(_callType),
+ ),
+ ),
+ ],
+ ),
+ )
+ ],
+ ),
+ ),
+ ),
+ ],
+ );
+ },
+ )),
+ );
+ }
+
+ _getCallTitle() {
+ log('[_getCallTitle]', TAG);
+ String callType = _callType == CallType.VIDEO_CALL ? "Video" : 'Audio';
+ return "Incoming $callType call";
+ }
+
+ void _acceptCall(int callType) async {
+ log('[_acceptCall]', TAG);
+ _callManager.startNewIncomingCall(context, _currentUser, _callId,
+ _meetingId, callType, _callName, _initiatorId, _participantIds, false,
+ initialLocalMediaStream: _localMediaStream,
+ isFrontCameraUsed: _isFrontCameraSelected);
+ }
+
+ void _rejectCall() {
+ log('[_rejectCall]', TAG);
+ _localMediaStream?.getTracks().forEach((track) async {
+ await track.stop();
+ });
+ _localMediaStream?.dispose();
+
+ _callManager.reject(_callId, _meetingId, false, _initiatorId, false);
+ closeScreen();
+ }
+
+ Future _onBackPressed(BuildContext context) {
+ return Future.value(false);
+ }
+
+ @override
+ void dispose() {
+ log('[dispose]', TAG);
+ _localVideoRenderer?.srcObject = null;
+ _localVideoRenderer?.dispose();
+
+ _callManager.onCloseCall = null;
+ _callManager.onCallAccepted = null;
+ _callManager.onCallRejected = null;
+
+ _stopRingtone();
+
+ super.dispose();
+ }
+
+ void _onCallClosed() {
+ log('[_onCallClosed]', TAG);
+ _localMediaStream?.getTracks().forEach((track) async {
+ await track.stop();
+ });
+ _localMediaStream?.dispose();
+
+ closeScreen();
+ }
+
+ void _onCallAccepted(String meetingId) {
+ log('[_onCallAccepted]', TAG);
+ }
+
+ void _onCallRejected(String meetingId) {
+ log('[_onCallRejected]', TAG);
+ if (meetingId == _meetingId) {
+ _localMediaStream?.getTracks().forEach((track) async {
+ await track.stop();
+ });
+ _localMediaStream?.dispose();
+
+ closeScreen();
+ }
+ }
+
+ Future _getLocalMediaStream() {
+ log('[_getLocalMediaStream]', TAG);
+ if (_callType == CallType.AUDIO_CALL) return Future.value(null);
+ if (_localMediaStream != null) return Future.value(_localMediaStream);
+
+ return navigator.mediaDevices
+ .getUserMedia(getMediaConstraints())
+ .then((localMediaStream) {
+ _localMediaStream = localMediaStream;
+
+ return localMediaStream;
+ });
+ }
+
+ Map getMediaConstraints() {
+ log('[getMediaConstraints]', TAG);
+ final Map mediaConstraints = {
+ 'audio': getAudioConfig(),
+ };
+
+ if (CallType.VIDEO_CALL == _callType) {
+ mediaConstraints['video'] = getVideoConfig();
+ }
+
+ return mediaConstraints;
+ }
+
+ Future getVideoRenderer(MediaStream? mediaStream) {
+ log('[getVideoRenderer]', TAG);
+ if (_localVideoRenderer != null) return Future.value(_localVideoRenderer);
+
+ var videoRenderer = RTCVideoRenderer();
+
+ return videoRenderer.initialize().then((value) {
+ videoRenderer.srcObject = mediaStream;
+ _localVideoRenderer = videoRenderer;
+ return videoRenderer;
+ });
+ }
+
+ _muteMic() {
+ if (_localMediaStream == null) return;
+
+ setState(() {
+ _isMicMute = !_isMicMute;
+
+ var audioTrack = _localMediaStream?.getAudioTracks().firstOrNull;
+
+ if (audioTrack != null) {
+ Helper.setMicrophoneMute(_isMicMute, audioTrack);
+ }
+ });
+ }
+
+ _toggleCamera() {
+ if (_localMediaStream == null) return;
+
+ setState(() {
+ _localMediaStream?.getVideoTracks().firstOrNull?.enabled =
+ !isVideoEnabledInStream(_localMediaStream);
+ });
+ }
+
+ _switchCamera() {
+ if (_localMediaStream == null) return;
+
+ if (!isVideoEnabledInStream(_localMediaStream)) return;
+
+ if (!kIsWeb && (Platform.isAndroid || Platform.isIOS)) {
+ switchCamera().then((isFrontCameraUsed) {
+ setState(() {
+ _isFrontCameraSelected = isFrontCameraUsed;
+ });
+ });
+ } else {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return FutureBuilder>(
+ future: Helper.cameras,
+ builder: (context, snapshot) {
+ if (!snapshot.hasData || snapshot.data!.isEmpty) {
+ return AlertDialog(
+ content: const Text('No cameras found'),
+ actions: [
+ TextButton(
+ style: TextButton.styleFrom(
+ textStyle: Theme.of(context).textTheme.labelLarge,
+ ),
+ child: const Text('Ok'),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ )
+ ],
+ );
+ } else {
+ return SimpleDialog(
+ title: const Text('Select camera'),
+ children: snapshot.data?.map(
+ (mediaDeviceInfo) {
+ return SimpleDialogOption(
+ onPressed: () {
+ Navigator.pop(context, mediaDeviceInfo.deviceId);
+ },
+ child: Text(mediaDeviceInfo.label),
+ );
+ },
+ ).toList(),
+ );
+ }
+ },
+ );
+ },
+ ).then((deviceId) {
+ log("onCameraSelected deviceId: $deviceId", TAG);
+ if (deviceId != null) switchCamera(deviceId: deviceId);
+ });
+ }
+ }
+
+ bool isVideoEnabledInStream(MediaStream? mediaStream) {
+ if (mediaStream == null) return false;
+
+ if (mediaStream.getVideoTracks().isEmpty) return false;
+
+ return mediaStream.getVideoTracks().first.enabled;
+ }
+
+ Future switchCamera({String? deviceId}) async {
+ try {
+ if (_localMediaStream == null) {
+ return Future.error(IllegalStateException(
+ "Can't perform operation [switchCamera], cause 'localStream' not initialised"));
+ } else {
+ if (deviceId != null) {
+ var newMediaStream = await navigator.mediaDevices.getUserMedia({
+ 'audio': false,
+ 'video': kIsWeb
+ ? {'deviceId': deviceId}
+ : getVideoConfig(deviceId: deviceId),
+ });
+
+ var oldVideoTrack = _localMediaStream!.getVideoTracks()[0];
+
+ await _localMediaStream?.removeTrack(oldVideoTrack);
+ oldVideoTrack.stop();
+
+ await _localMediaStream?.addTrack(newMediaStream.getVideoTracks()[0]);
+
+ return Future.value(true);
+ } else {
+ final videoTrack = _localMediaStream!.getVideoTracks()[0];
+ return Helper.switchCamera(videoTrack, null, _localMediaStream);
+ }
+ }
+ } catch (error) {
+ return Future.error(error);
+ }
+ }
+
+ closeScreen() {
+ Navigator.of(context).pop();
+ }
+
+ void _playRingtone() {
+ _ringtonePlayer.open(Audio("assets/audio/calling.mp3"),
+ loopMode: LoopMode.single);
+ }
+
+ void _stopRingtone() {
+ _ringtonePlayer.stop();
+ }
+}
diff --git a/conf_call_sample/lib/src/login_screen.dart b/conf_call_sample/lib/src/screens/login_screen.dart
similarity index 67%
rename from conf_call_sample/lib/src/login_screen.dart
rename to conf_call_sample/lib/src/screens/login_screen.dart
index 3d2fce7..77c383e 100644
--- a/conf_call_sample/lib/src/login_screen.dart
+++ b/conf_call_sample/lib/src/screens/login_screen.dart
@@ -2,8 +2,10 @@ import 'package:flutter/material.dart';
import 'package:connectycube_sdk/connectycube_sdk.dart';
+import '../managers/call_manager.dart';
+import '../utils/configs.dart' as utils;
+import '../utils/pref_util.dart';
import 'select_opponents_screen.dart';
-import 'utils/configs.dart' as utils;
class LoginScreen extends StatelessWidget {
static const String TAG = "LoginScreen";
@@ -31,8 +33,28 @@ class BodyState extends State {
bool _isLoginContinues = false;
int? _selectedUserId;
+ @override
+ void initState() {
+ super.initState();
+ log("initState", TAG);
+
+ _loginWithSavedUserIfExist();
+
+ CallManager.startCallIfNeed(context);
+ }
+
+ void _loginWithSavedUserIfExist() {
+ SharedPrefs.getUser().then((savedUser) {
+ if (savedUser != null) {
+ _loginToCC(context, savedUser, savedUser: true);
+ }
+ });
+ }
+
@override
Widget build(BuildContext context) {
+ log("build", TAG);
+
return Padding(
padding: EdgeInsets.all(48),
child: Column(
@@ -53,6 +75,7 @@ class BodyState extends State {
}
Widget _getUsersList(BuildContext context) {
+ log("[_getUsersList]", TAG);
final users = utils.users;
return ListView.builder(
@@ -97,9 +120,18 @@ class BodyState extends State {
);
}
- _loginToCC(BuildContext context, CubeUser user) {
+ _loginToCC(BuildContext context, CubeUser user, {bool savedUser = false}) {
+ log('[_loginToCC]', TAG);
if (_isLoginContinues) return;
+ if (CubeSessionManager.instance.isActiveSessionValid() &&
+ CubeChatConnection.instance.chatConnectionState ==
+ CubeChatConnectionState.Ready &&
+ CubeChatConnection.instance.currentUser?.id == user.id) {
+ _goSelectOpponentsScreen(context, user);
+ return;
+ }
+
setState(() {
_isLoginContinues = true;
_selectedUserId = user.id;
@@ -111,6 +143,10 @@ class BodyState extends State {
_loginToCubeChat(context, user);
} else {
createSession(user).then((cubeSession) {
+ if (!savedUser) {
+ SharedPrefs.saveNewUser(user);
+ CallManager.instance.init(context);
+ }
_loginToCubeChat(context, user);
}).catchError((onError) {
_processLoginError(onError);
@@ -119,12 +155,15 @@ class BodyState extends State {
}
void _loginToCubeChat(BuildContext context, CubeUser user) {
+ log('[_loginToCubeChat]', TAG);
CubeChatConnection.instance.login(user).then((cubeUser) {
- setState(() {
- _isLoginContinues = false;
- _selectedUserId = 0;
- });
- _goSelectOpponentsScreen(context, cubeUser);
+ if (mounted) {
+ setState(() {
+ _isLoginContinues = false;
+ _selectedUserId = 0;
+ });
+ _goSelectOpponentsScreen(context, cubeUser);
+ }
}).catchError((onError) {
_processLoginError(onError);
});
@@ -132,6 +171,7 @@ class BodyState extends State {
void _processLoginError(exception) {
log("Login error $exception", TAG);
+ if (!mounted) return;
setState(() {
_isLoginContinues = false;
@@ -155,11 +195,37 @@ class BodyState extends State {
}
void _goSelectOpponentsScreen(BuildContext context, CubeUser cubeUser) {
- Navigator.push(
- context,
- MaterialPageRoute(
- builder: (context) => SelectOpponentsScreen(cubeUser),
- ),
- );
+ if (!CallManager.instance.hasActiveCall()) {
+ Navigator.of(context).pushReplacement(
+ MaterialPageRoute(
+ builder: (context) => SelectOpponentsScreen(cubeUser),
+ ),
+ );
+ }
+ }
+
+ @override
+ void dispose() {
+ log("[dispose]", TAG);
+
+ super.dispose();
+ }
+
+ @override
+ void deactivate() {
+ super.deactivate();
+ log("[deactivate]", TAG);
+ }
+
+ @override
+ void activate() {
+ super.activate();
+ log("[activate]", TAG);
+ }
+
+ @override
+ void didChangeDependencies() {
+ super.didChangeDependencies();
+ log("[didChangeDependencies]", TAG);
}
}
diff --git a/conf_call_sample/lib/src/select_opponents_screen.dart b/conf_call_sample/lib/src/screens/select_opponents_screen.dart
similarity index 64%
rename from conf_call_sample/lib/src/select_opponents_screen.dart
rename to conf_call_sample/lib/src/screens/select_opponents_screen.dart
index ca66c94..e7c0502 100644
--- a/conf_call_sample/lib/src/select_opponents_screen.dart
+++ b/conf_call_sample/lib/src/screens/select_opponents_screen.dart
@@ -2,10 +2,13 @@ import 'package:flutter/material.dart';
import 'package:connectycube_sdk/connectycube_sdk.dart';
-import 'call_screen.dart';
-import 'utils/configs.dart' as utils;
-import 'utils/call_manager.dart';
-import 'utils/platform_utils.dart';
+import '../managers/call_manager.dart';
+import '../utils/configs.dart' as utils;
+import '../utils/platform_utils.dart';
+import '../utils/pref_util.dart';
+import 'conversation_call_screen.dart';
+import 'incoming_call_screen.dart';
+import 'login_screen.dart';
class SelectOpponentsScreen extends StatelessWidget {
final CubeUser currentUser;
@@ -38,7 +41,7 @@ class SelectOpponentsScreen extends StatelessWidget {
}
Future _onBackPressed() {
- return Future.value(false);
+ return Future.value(true);
}
_logOut(BuildContext context) {
@@ -61,6 +64,7 @@ class SelectOpponentsScreen extends StatelessWidget {
signOut().then(
(voidValue) {
CubeChatConnection.instance.destroy();
+ SharedPrefs.deleteUserData();
Navigator.pop(context); // cancel current Dialog
_navigateToLoginScreen(context);
},
@@ -79,7 +83,11 @@ class SelectOpponentsScreen extends StatelessWidget {
}
_navigateToLoginScreen(BuildContext context) {
- Navigator.pop(context);
+ Navigator.of(context).pushReplacement(
+ MaterialPageRoute(
+ builder: (context) => LoginScreen(),
+ ),
+ );
}
}
@@ -95,16 +103,16 @@ class BodyLayout extends StatefulWidget {
}
class _BodyLayoutState extends State {
+ static final String TAG = 'SelectOpponentsScreen';
+
Set _selectedUsers = {};
final CubeUser _currentUser;
- late CallManager _callManager;
- late ConferenceClient _callClient;
- ConferenceSession? _currentCall;
_BodyLayoutState(this._currentUser);
@override
Widget build(BuildContext context) {
+ log('[build]', TAG);
return Container(
padding: EdgeInsets.all(48),
child: Column(
@@ -113,15 +121,10 @@ class _BodyLayoutState extends State {
"Select users to start call:",
style: TextStyle(fontSize: 22),
),
- Expanded(
- child: _getOpponentsList(),
- ),
+ _getOpponentsList(),
Row(
mainAxisSize: MainAxisSize.min,
children: [
- Container(
- width: 24,
- ),
FloatingActionButton(
heroTag: "VideoCall",
child: Icon(
@@ -129,7 +132,21 @@ class _BodyLayoutState extends State {
color: Colors.white,
),
backgroundColor: Colors.blue,
- onPressed: () => _startCall(_selectedUsers),
+ onPressed: () =>
+ _startCall(_selectedUsers, CallType.VIDEO_CALL),
+ ),
+ Container(
+ width: 32,
+ ),
+ FloatingActionButton(
+ heroTag: "AudioCall",
+ child: Icon(
+ Icons.call,
+ color: Colors.white,
+ ),
+ backgroundColor: Colors.green,
+ onPressed: () =>
+ _startCall(_selectedUsers, CallType.AUDIO_CALL),
),
],
),
@@ -138,12 +155,15 @@ class _BodyLayoutState extends State {
}
Widget _getOpponentsList() {
+ log('[_getOpponentsList]', TAG);
CubeUser? currentUser = _currentUser;
final users =
utils.users.where((user) => user.id != currentUser.id).toList();
return ListView.builder(
+ shrinkWrap: true,
itemCount: users.length,
itemBuilder: (context, index) {
+ log('[itemBuilder] index $index', TAG);
return Card(
child: CheckboxListTile(
title: Center(
@@ -153,6 +173,7 @@ class _BodyLayoutState extends State {
),
value: _selectedUsers.contains(users[index].id),
onChanged: ((checked) {
+ log('[CheckboxListTile][onChanged]', TAG);
setState(() {
if (checked!) {
_selectedUsers.add(users[index].id!);
@@ -170,30 +191,28 @@ class _BodyLayoutState extends State {
@override
void initState() {
super.initState();
+ log('[initState]', TAG);
initForegroundService();
+ checkSystemAlertWindowPermission(context);
+ requestNotificationsPermission();
+ CallManager.instance.context = context;
+ requestFullScreenIntentsPermission(context);
- CubeSettings.instance.onSessionRestore = () {
- return createSession(_currentUser);
- };
-
- _initConferenceConfig();
_initCalls();
}
void _initCalls() {
- _callClient = ConferenceClient.instance;
- _callManager = CallManager.instance;
- _callManager.onReceiveNewCall = (meetingId, participantIds) {
- _showIncomingCallScreen(meetingId, participantIds);
- };
-
- _callManager.onCloseCall = () {
- _currentCall = null;
+ log('[_initCalls]', TAG);
+ CallManager.instance.onReceiveNewCall =
+ (callId, meetingId, initiatorId, participantIds, callType, callName) {
+ _showIncomingCallScreen(
+ callId, meetingId, initiatorId, participantIds, callType, callName);
};
}
- void _startCall(Set opponents) async {
+ void _startCall(Set opponents, int callType) async {
+ log('[_startCall] call type $callType', TAG);
if (opponents.isEmpty) return;
var attendees = opponents.map((entry) {
@@ -210,31 +229,34 @@ class _BodyLayoutState extends State {
attendees: attendees,
);
createMeeting(meeting).then((createdMeeting) async {
- _currentCall = await _callClient.createCallSession(
+ var callSession = await ConferenceClient.instance.createCallSession(
createdMeeting.hostId!,
- callType: CallType.VIDEO_CALL,
+ callType: callType,
);
- Navigator.push(
- context,
+ Navigator.of(context).push(
MaterialPageRoute(
- builder: (context) => ConversationCallScreen(_currentCall!,
- createdMeeting.meetingId!, opponents.toList(), false),
+ builder: (context) => ConversationCallScreen(
+ _currentUser,
+ callSession,
+ createdMeeting.meetingId!,
+ opponents.toList(),
+ false,
+ '${_currentUser.fullName ?? 'Unknown User'}${opponents.length > 1 ? ' (in Group call)' : ''}'),
),
);
});
}
- void _showIncomingCallScreen(String meetingId, List participantIds) {
- Navigator.push(
- context,
+ void _showIncomingCallScreen(String callId, String meetingId, int initiatorId,
+ List participantIds, int callType, String callName) {
+ log('[_showIncomingCallScreen]', TAG);
+
+ Navigator.of(context).push(
MaterialPageRoute(
- builder: (context) => IncomingCallScreen(meetingId, participantIds),
+ builder: (context) => IncomingCallScreen(_currentUser, callId,
+ meetingId, initiatorId, participantIds, callType, callName),
),
);
}
-
- void _initConferenceConfig() {
- ConferenceConfig.instance.url = utils.SERVER_ENDPOINT;
- }
}
diff --git a/conf_call_sample/lib/src/utils/call_manager.dart b/conf_call_sample/lib/src/utils/call_manager.dart
deleted file mode 100644
index e2955b1..0000000
--- a/conf_call_sample/lib/src/utils/call_manager.dart
+++ /dev/null
@@ -1,198 +0,0 @@
-import 'dart:async';
-
-import 'package:connectycube_sdk/connectycube_sdk.dart';
-
-const NO_ANSWER_TIMER_INTERVAL = 30;
-
-class CallManager {
- SystemMessagesManager? _systemMessagesManager;
- NewCallCallback? onReceiveNewCall;
- CloseCall? onCloseCall;
- RejectCallCallback? onReceiveRejectCall;
- UserNotAnswerCallback? onUserNotAnswerCallback;
- String? _meetingId;
- List? _participantIds;
- int? _initiatorId;
-
- var _answerUserTimers = Map();
-
- CallManager._privateConstructor() {
- _systemMessagesManager = CubeChatConnection.instance.systemMessagesManager;
- _systemMessagesManager!.systemMessagesStream
- .listen((cubeMessage) => parseCallMessage(cubeMessage));
-
- RTCConfig.instance.statsReportsInterval = 200;
- }
-
- static final CallManager _instance = CallManager._privateConstructor();
-
- static CallManager get instance => _instance;
-
- parseCallMessage(CubeMessage cubeMessage) {
- log("parseCallMessage cubeMessage= $cubeMessage");
- final properties = cubeMessage.properties;
- var meetingId = properties["meetingId"];
-
- if (properties.containsKey("callStart")) {
- var participantIds = properties["participantIds"]!
- .split(',')
- .map((id) => int.parse(id))
- .toList();
- if (_meetingId == null) {
- _meetingId = meetingId;
- _initiatorId = cubeMessage.senderId;
- _participantIds = participantIds;
- if (onReceiveNewCall != null) {
- onReceiveNewCall!(meetingId!, participantIds);
- }
- }
- } else if (properties.containsKey("callAccepted")) {
- if (_meetingId == meetingId) {
- _clearNoAnswerTimers(id: cubeMessage.senderId!);
- }
- } else if (properties.containsKey("callRejected")) {
- bool isBusy = properties["busy"] == 'true';
- if (_meetingId == meetingId) {
- if (onReceiveRejectCall != null) {
- onReceiveRejectCall!(meetingId!, cubeMessage.senderId!, isBusy);
- }
-
- handleRejectCall(cubeMessage.senderId!, isBusy);
- }
- } else if (properties.containsKey("callEnd")) {
- if (_meetingId == meetingId) {
- _clearCall(cubeMessage.senderId!);
- }
- }
- }
-
- startCall(String meetingId, List participantIds, int currentUserId) {
- _initiatorId = currentUserId;
- _participantIds = participantIds;
- _meetingId = meetingId;
- sendCallMessage(meetingId, participantIds);
- startNoAnswerTimers(participantIds);
- }
-
- acceptCall(String meetingId, int participantId) {
- sendAcceptMessage(meetingId, participantId);
- }
-
- reject(String meetingId, bool isBusy) {
- sendRejectMessage(meetingId, isBusy, _initiatorId!);
- _clearProperties();
- }
-
- stopCall() {
- _clearNoAnswerTimers();
- sendEndCallMessage(_meetingId!, _participantIds!);
- _clearProperties();
- }
-
- sendCallMessage(String meetingId, List participantIds) {
- List callMsgList =
- _buildCallMessages(meetingId, participantIds);
- callMsgList.forEach((callMsg) {
- callMsg.properties['callStart'] = '1';
- callMsg.properties['participantIds'] = participantIds.join(',');
- });
- callMsgList
- .forEach((msg) => _systemMessagesManager!.sendSystemMessage(msg));
- }
-
- sendAcceptMessage(String meetingId, int participantId) {
- List callMsgList =
- _buildCallMessages(meetingId, [participantId]);
- callMsgList.forEach((callMsg) {
- callMsg.properties['callAccepted'] = '1';
- });
- callMsgList
- .forEach((msg) => _systemMessagesManager!.sendSystemMessage(msg));
- }
-
- sendRejectMessage(String meetingId, bool isBusy, int participantId) {
- List callMsgList =
- _buildCallMessages(meetingId, [participantId]);
- callMsgList.forEach((callMsg) {
- callMsg.properties['callRejected'] = '1';
- callMsg.properties['busy'] = isBusy.toString();
- });
- callMsgList
- .forEach((msg) => _systemMessagesManager!.sendSystemMessage(msg));
- }
-
- sendEndCallMessage(String meetingId, List participantIds) {
- List callMsgList =
- _buildCallMessages(meetingId, participantIds);
- callMsgList.forEach((callMsg) {
- callMsg.properties['callEnd'] = '1';
- });
- callMsgList
- .forEach((msg) => _systemMessagesManager!.sendSystemMessage(msg));
- }
-
- List _buildCallMessages(
- String meetingId, List participantIds) {
- return participantIds.map((userId) {
- var msg = CubeMessage();
- msg.recipientId = userId;
- msg.properties = {'meetingId': meetingId};
- return msg;
- }).toList();
- }
-
- handleAcceptCall(int participantId) {
- _clearNoAnswerTimers(id: participantId);
- }
-
- handleRejectCall(int participantId, isBusy) {
- _clearNoAnswerTimers(id: participantId);
- _clearCall(participantId);
- }
-
- startNoAnswerTimers(participantIds) {
- participantIds.forEach((userId) => {
- _answerUserTimers[userId] = Timer(
- Duration(seconds: NO_ANSWER_TIMER_INTERVAL),
- () => noUserAnswer(userId))
- });
- }
-
- noUserAnswer(int participantId) {
- if (onUserNotAnswerCallback != null)
- onUserNotAnswerCallback!(participantId);
- _clearNoAnswerTimers(id: participantId);
- sendEndCallMessage(_meetingId!, [participantId]);
- _clearCall(participantId);
- }
-
- _clearNoAnswerTimers({int id = 0}) {
- if (id != 0) {
- _answerUserTimers[id]!.cancel();
- _answerUserTimers.remove(id);
- } else {
- _answerUserTimers.forEach((participantId, timer) => timer.cancel());
- _answerUserTimers.clear();
- }
- }
-
- _clearProperties() {
- _meetingId = null;
- _initiatorId = null;
- _participantIds = null;
- }
-
- _clearCall(int participantId) {
- _participantIds!.remove(participantId);
- if (_participantIds!.isEmpty || participantId == _initiatorId) {
- _clearProperties();
- if (onCloseCall != null) onCloseCall!();
- }
- }
-}
-
-typedef void NewCallCallback(String meetingId, List participantIds);
-typedef void CloseCall();
-typedef void RejectCallCallback(
- String meetingId, int participantId, bool isBusy);
-typedef void UserNotAnswerCallback(int participantId);
diff --git a/conf_call_sample/lib/src/utils/configs.dart b/conf_call_sample/lib/src/utils/configs.dart
index 993a383..554fac8 100644
--- a/conf_call_sample/lib/src/utils/configs.dart
+++ b/conf_call_sample/lib/src/utils/configs.dart
@@ -1,11 +1,12 @@
import 'package:connectycube_sdk/connectycube_sdk.dart';
-const String APP_ID = "476";
-const String AUTH_KEY = "PDZjPBzAO8WPfCp";
-const String AUTH_SECRET = "6247kjxXCLRaua6";
-const String ACCOUNT_ID = "TpuBZox_HPxofh7PVZdP";
-const String DEFAULT_PASS = "xxasBUM3gQs36bhj";
-const String SERVER_ENDPOINT = "wss://janus.connectycube.com:8989";
+const String APP_ID = '476';
+const String AUTH_KEY = 'PDZjPBzAO8WPfCp';
+const String AUTH_SECRET = '6247kjxXCLRaua6';
+const String DEFAULT_PASS = 'xxasBUM3gQs36bhj';
+const String API_ENDPOINT = 'https://api.connectycube.com';
+const String CHAT_ENDPOINT = 'chat.connectycube.com';
+const String CONF_SERVER_ENDPOINT = 'wss://janus.connectycube.com:8989';
List users = [
CubeUser(
diff --git a/conf_call_sample/lib/src/utils/consts.dart b/conf_call_sample/lib/src/utils/consts.dart
new file mode 100644
index 0000000..dd75268
--- /dev/null
+++ b/conf_call_sample/lib/src/utils/consts.dart
@@ -0,0 +1,24 @@
+final String PARAM_SESSION_ID = 'session_id';
+final String PARAM_CALL_TYPE = 'call_type';
+final String PARAM_CALLER_ID = 'caller_id';
+final String PARAM_CALLER_NAME = 'caller_name';
+final String PARAM_CALL_OPPONENTS = 'call_opponents';
+final String PARAM_IOS_VOIP = 'ios_voip';
+final String PARAM_SIGNAL_TYPE = 'signal_type';
+final String PARAM_EXPIRATION = 'expiration';
+final String PARAM_USER_INFO = 'user_info';
+final String PARAM_MEETING_ID = 'meeting_id';
+final String PARAM_MESSAGE = 'message';
+final String PARAM_BUSY = 'busy';
+final String PARAM_PHOTO_URL = 'photo_url';
+
+final String SIGNAL_TYPE_START_CALL = 'startCall';
+final String SIGNAL_TYPE_END_CALL = 'endCall';
+final String SIGNAL_TYPE_ACCEPT_CALL = 'acceptCall';
+final String SIGNAL_TYPE_REJECT_CALL = 'rejectCall';
+final String SIGNAL_TYPE_REQUEST_MEDIA_STATE = 'requestMediaState';
+final String SIGNAL_TYPE_UPDATE_MEDIA_STATE = 'updateMediaState';
+
+final String PARAM_MEDIA_CONFIG = 'media_config';
+final String PARAM_IS_MIC_ENABLED = 'is_mic_enabled';
+final String PARAM_IS_CAMERA_ENABLED = 'is_camera_enabled';
diff --git a/conf_call_sample/lib/src/utils/duration_timer.dart b/conf_call_sample/lib/src/utils/duration_timer.dart
new file mode 100644
index 0000000..3f02073
--- /dev/null
+++ b/conf_call_sample/lib/src/utils/duration_timer.dart
@@ -0,0 +1,26 @@
+import 'dart:async';
+
+class DurationTimer {
+ int _durationSec = 0;
+
+ Timer? _durationTimer;
+ StreamController _durationStreamController =
+ StreamController.broadcast();
+
+ Stream get durationStream => _durationStreamController.stream;
+
+ start() {
+ if (_durationTimer == null) {
+ _durationTimer = Timer.periodic(Duration(seconds: 1), (timer) async {
+ _durationSec++;
+ _durationStreamController.add(_durationSec);
+ });
+ }
+ }
+
+ stop() {
+ _durationTimer?.cancel();
+ _durationTimer = null;
+ _durationSec = 0;
+ }
+}
diff --git a/conf_call_sample/lib/src/utils/media_utils.dart b/conf_call_sample/lib/src/utils/media_utils.dart
new file mode 100644
index 0000000..9e437d8
--- /dev/null
+++ b/conf_call_sample/lib/src/utils/media_utils.dart
@@ -0,0 +1,75 @@
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+import 'consts.dart';
+
+bool canShowVideo(
+ int? userId, MediaStream? mediaStream, Map> config) {
+ if (userId == null || mediaStream == null) return false;
+
+ if (mediaStream.getVideoTracks().isEmpty) return false;
+
+ var hasEnabledVideo = false;
+
+ mediaStream.getVideoTracks().forEach((videoTrack) {
+ if (!hasEnabledVideo && videoTrack.enabled) {
+ hasEnabledVideo = true;
+ }
+ });
+
+ return hasEnabledVideo && isUserCameraEnabled(userId, config);
+}
+
+bool isUserCameraEnabled(int userId, Map> config,
+ {bool defaultValue = false}) {
+ return config[userId]?[PARAM_IS_CAMERA_ENABLED] ?? defaultValue;
+}
+
+int? getUserWithEnabledVideo(Map renderers,
+ int currentUserId, Map> config) {
+ var resultUserId = -1;
+
+ renderers.forEach((userId, renderer) {
+ if ((resultUserId == -1 || resultUserId == currentUserId) &&
+ canShowVideo(userId, renderer.srcObject, config)) {
+ resultUserId = userId;
+ }
+ });
+
+ return resultUserId == -1 ? null : resultUserId;
+}
+
+void chooseOpponentsStreamsQuality(ConferenceSession callSession,
+ int currentUserId, Map config) {
+ config.remove(currentUserId);
+
+ if (config.isEmpty) return;
+
+ callSession.requestPreferredStreamsForOpponents(config);
+}
+
+void updatePrimaryUser(
+ int userId,
+ bool force,
+ int currentUserId,
+ MapEntry? primaryRenderer,
+ Map minorRenderers,
+ Map> participantsMediaConfigs, {
+ required Function(MapEntry? primaryRenderer,
+ Map minorRenderers)?
+ onRenderersUpdated,
+}) {
+ if (!minorRenderers.containsKey(userId) ||
+ userId == primaryRenderer?.key ||
+ (userId == currentUserId && !force) ||
+ getUserWithEnabledVideo(
+ minorRenderers, currentUserId, participantsMediaConfigs) ==
+ null) return;
+
+ if (primaryRenderer?.key != userId) {
+ minorRenderers.addEntries([primaryRenderer!]);
+ }
+
+ primaryRenderer = MapEntry(userId, minorRenderers.remove(userId)!);
+
+ onRenderersUpdated?.call(primaryRenderer, minorRenderers);
+}
diff --git a/conf_call_sample/lib/src/utils/platform_utils.dart b/conf_call_sample/lib/src/utils/platform_utils.dart
index 4beb8cd..c1e776a 100644
--- a/conf_call_sample/lib/src/utils/platform_utils.dart
+++ b/conf_call_sample/lib/src/utils/platform_utils.dart
@@ -1,11 +1,18 @@
+import 'package:device_info_plus/device_info_plus.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
import 'package:flutter_background/flutter_background.dart';
+import 'package:permission_handler/permission_handler.dart';
import 'package:universal_io/io.dart';
+import 'package:connectycube_flutter_call_kit/connectycube_flutter_call_kit.dart';
+import 'package:connectycube_sdk/connectycube_calls.dart';
+
Future initForegroundService() async {
if (Platform.isAndroid) {
final androidConfig = FlutterBackgroundAndroidConfig(
notificationTitle: 'Conference Calls sample',
- notificationText: 'Screen sharing in in progress',
+ notificationText: 'Screen sharing is in progress',
notificationImportance: AndroidNotificationImportance.Default,
notificationIcon:
AndroidResource(name: 'ic_launcher_foreground', defType: 'drawable'),
@@ -41,3 +48,105 @@ Future hasBackgroundExecutionPermissions() async {
return Future.value(true);
}
}
+
+Future checkSystemAlertWindowPermission(BuildContext context) async {
+ if (Platform.isAndroid) {
+ var androidInfo = await DeviceInfoPlugin().androidInfo;
+ var sdkInt = androidInfo.version.sdkInt;
+
+ if (sdkInt >= 31) {
+ if (await Permission.systemAlertWindow.isDenied) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text('Permission required'),
+ content: Text(
+ 'For accepting the calls in the background you should provide access to show System Alerts from the background. Would you like to do it now?'),
+ actions: [
+ TextButton(
+ onPressed: () {
+ Permission.systemAlertWindow.request().then((status) {
+ if (status.isGranted) {
+ Navigator.of(context).pop();
+ }
+ });
+ },
+ child: Text(
+ 'Allow',
+ ),
+ ),
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: Text(
+ 'Later',
+ ),
+ ),
+ ],
+ );
+ },
+ );
+ }
+ }
+ }
+}
+
+requestNotificationsPermission() async {
+ if (!kIsWeb && (Platform.isAndroid || Platform.isIOS || Platform.isWindows)) {
+ var isPermissionGranted = await Permission.notification.isGranted;
+ if (!isPermissionGranted) {
+ await Permission.notification.request();
+ }
+ }
+}
+
+requestFullScreenIntentsPermission(BuildContext context) async {
+ if (!Platform.isAndroid) return;
+
+ var androidInfo = await DeviceInfoPlugin().androidInfo;
+ var sdkInt = androidInfo.version.sdkInt;
+
+ if (sdkInt < 34) return;
+
+ ConnectycubeFlutterCallKit.canUseFullScreenIntent()
+ .then((canUseFullScreenIntent) {
+ log('[requestFullScreenIntentsPermission] canUseFullScreenIntent: $canUseFullScreenIntent',
+ 'platform_utils');
+
+ if (!canUseFullScreenIntent) {
+ showDialog(
+ context: context,
+ builder: (BuildContext context) {
+ return AlertDialog(
+ title: Text('Full Screen notifications Permission required'),
+ content: Text(
+ 'To display an Incoming call on the Lock screen, you must grant access to the Lock screen. Would you like to do it now?'),
+ actions: [
+ TextButton(
+ onPressed: () {
+ ConnectycubeFlutterCallKit.provideFullScreenIntentAccess()
+ .then((_) {
+ Navigator.of(context).pop();
+ });
+ },
+ child: Text(
+ 'Grant',
+ ),
+ ),
+ TextButton(
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ child: Text(
+ 'Later',
+ ),
+ ),
+ ],
+ );
+ },
+ );
+ }
+ });
+}
diff --git a/conf_call_sample/lib/src/utils/pref_util.dart b/conf_call_sample/lib/src/utils/pref_util.dart
new file mode 100644
index 0000000..4367ea8
--- /dev/null
+++ b/conf_call_sample/lib/src/utils/pref_util.dart
@@ -0,0 +1,100 @@
+import 'dart:async';
+
+import 'package:shared_preferences/shared_preferences.dart';
+
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+const String prefUserLogin = "pref_user_login";
+const String prefUserPsw = "pref_user_psw";
+const String prefUserName = "pref_user_name";
+const String prefUserId = "pref_user_id";
+const String prefUserAvatar = "pref_user_avatar";
+const String prefSubscriptionToken = "pref_subscription_token";
+const String prefSubscriptionId = "pref_subscription_id";
+
+class SharedPrefs {
+ static SharedPreferences? _prefs;
+
+ static Future getPrefs() async {
+ Completer completer = Completer();
+ if (_prefs != null) {
+ completer.complete(_prefs);
+ } else {
+ _prefs = await SharedPreferences.getInstance();
+ completer.complete(_prefs);
+ }
+ return completer.future;
+ }
+
+ static Future saveNewUser(CubeUser cubeUser) {
+ return getPrefs().then((prefs) {
+ prefs.clear();
+ prefs.setString(prefUserLogin, cubeUser.login!);
+ prefs.setString(prefUserPsw, cubeUser.password!);
+ prefs.setString(prefUserName, cubeUser.fullName!);
+ prefs.setInt(prefUserId, cubeUser.id!);
+ if (cubeUser.avatar != null)
+ prefs.setString(prefUserAvatar, cubeUser.avatar!);
+
+ return Future.value(true);
+ });
+ }
+
+ static Future updateUser(CubeUser cubeUser) {
+ return getPrefs().then((prefs) {
+ if (cubeUser.password != null)
+ prefs.setString(prefUserPsw, cubeUser.password!);
+ if (cubeUser.login != null)
+ prefs.setString(prefUserLogin, cubeUser.login!);
+ if (cubeUser.fullName != null)
+ prefs.setString(prefUserName, cubeUser.fullName!);
+ if (cubeUser.avatar != null)
+ prefs.setString(prefUserAvatar, cubeUser.avatar!);
+
+ return Future.value(true);
+ });
+ }
+
+ static Future getUser() {
+ return getPrefs().then((prefs) {
+ if (prefs.getString(prefUserLogin) == null) return Future.value();
+ var user = CubeUser();
+ user.login = prefs.getString(prefUserLogin);
+ user.password = prefs.getString(prefUserPsw);
+ user.fullName = prefs.getString(prefUserName);
+ user.id = prefs.getInt(prefUserId);
+ user.avatar = prefs.getString(prefUserAvatar);
+ return Future.value(user);
+ });
+ }
+
+ static Future deleteUserData() {
+ return getPrefs().then((prefs) {
+ return prefs.clear();
+ });
+ }
+
+ static Future saveSubscriptionToken(String token) {
+ return getPrefs().then((prefs) {
+ return prefs.setString(prefSubscriptionToken, token);
+ });
+ }
+
+ static Future getSubscriptionToken() {
+ return getPrefs().then((prefs) {
+ return Future.value(prefs.getString(prefSubscriptionToken) ?? "");
+ });
+ }
+
+ static Future saveSubscriptionId(int id) {
+ return getPrefs().then((prefs) {
+ return prefs.setInt(prefSubscriptionId, id);
+ });
+ }
+
+ static Future getSubscriptionId() {
+ return getPrefs().then((prefs) {
+ return Future.value(prefs.getInt(prefSubscriptionId) ?? 0);
+ });
+ }
+}
diff --git a/conf_call_sample/lib/src/utils/string_utils.dart b/conf_call_sample/lib/src/utils/string_utils.dart
new file mode 100644
index 0000000..e5d2c2f
--- /dev/null
+++ b/conf_call_sample/lib/src/utils/string_utils.dart
@@ -0,0 +1,15 @@
+String formatHHMMSS(int seconds) {
+ int hours = (seconds / 3600).truncate();
+ seconds = (seconds % 3600).truncate();
+ int minutes = (seconds / 60).truncate();
+
+ String hoursStr = (hours).toString().padLeft(2, '0');
+ String minutesStr = (minutes).toString().padLeft(2, '0');
+ String secondsStr = (seconds % 60).toString().padLeft(2, '0');
+
+ if (hours == 0) {
+ return '$minutesStr:$secondsStr';
+ }
+
+ return '$hoursStr:$minutesStr:$secondsStr';
+}
diff --git a/conf_call_sample/lib/src/utils/video_config.dart b/conf_call_sample/lib/src/utils/video_config.dart
index d3f41d8..e6be5b7 100644
--- a/conf_call_sample/lib/src/utils/video_config.dart
+++ b/conf_call_sample/lib/src/utils/video_config.dart
@@ -5,6 +5,6 @@ class VideoQuality {
const VideoQuality(this.width, this.height);
}
-const HD_VIDEO = const VideoQuality (1280, 720);
-const VGA_VIDEO = const VideoQuality (640, 480);
-const QVGA_VIDEO = const VideoQuality (320, 240);
\ No newline at end of file
+const HD_VIDEO = const VideoQuality(1280, 720);
+const VGA_VIDEO = const VideoQuality(640, 480);
+const QVGA_VIDEO = const VideoQuality(320, 240);
diff --git a/conf_call_sample/lib/src/widgets/call_controls_widget.dart b/conf_call_sample/lib/src/widgets/call_controls_widget.dart
new file mode 100644
index 0000000..e53c957
--- /dev/null
+++ b/conf_call_sample/lib/src/widgets/call_controls_widget.dart
@@ -0,0 +1,180 @@
+import 'package:flutter/foundation.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter_speed_dial/flutter_speed_dial.dart';
+import 'package:web_browser_detect/web_browser_detect.dart';
+
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+class CallControls extends StatelessWidget {
+ final bool isMicMuted;
+ final Function() onMute;
+
+ final bool isCameraButtonVisible;
+ final bool isCameraEnabled;
+ final Function() onToggleCamera;
+
+ final bool isScreenSharingButtonVisible;
+ final bool isScreenSharingEnabled;
+ final Function() onToggleScreenSharing;
+
+ final bool isSpeakerEnabled;
+ final Function() onSwitchSpeaker;
+
+ final Function() onSwitchAudioInput;
+
+ final bool isSwitchCameraButtonVisible;
+ final Function() onSwitchCamera;
+
+ final Function() onEndCall;
+
+ CallControls({
+ super.key,
+ required this.isMicMuted,
+ required this.onMute,
+ required this.isCameraButtonVisible,
+ required this.isCameraEnabled,
+ required this.onToggleCamera,
+ required this.isScreenSharingButtonVisible,
+ required this.isScreenSharingEnabled,
+ required this.onToggleScreenSharing,
+ required this.isSpeakerEnabled,
+ required this.onSwitchSpeaker,
+ required this.onSwitchAudioInput,
+ required this.isSwitchCameraButtonVisible,
+ required this.onSwitchCamera,
+ required this.onEndCall,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ margin: EdgeInsets.only(bottom: 16, left: 8, right: 8),
+ child: ClipRRect(
+ borderRadius: BorderRadius.only(
+ bottomLeft: Radius.circular(32),
+ bottomRight: Radius.circular(32),
+ topLeft: Radius.circular(32),
+ topRight: Radius.circular(32)),
+ child: Container(
+ padding: EdgeInsets.all(4),
+ color: Colors.black26,
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Padding(
+ padding: EdgeInsets.only(right: 4),
+ child: FloatingActionButton(
+ elevation: 0,
+ heroTag: "Mute",
+ child: Icon(
+ isMicMuted ? Icons.mic_off : Icons.mic,
+ color: isMicMuted ? Colors.grey : Colors.white,
+ ),
+ onPressed: onMute,
+ backgroundColor: Colors.black38,
+ ),
+ ),
+ Visibility(
+ visible: isCameraButtonVisible,
+ child: Padding(
+ padding: EdgeInsets.only(right: 4),
+ child: FloatingActionButton(
+ elevation: 0,
+ heroTag: "ToggleCamera",
+ child: Icon(
+ isCameraEnabled ? Icons.videocam : Icons.videocam_off,
+ color: isCameraEnabled ? Colors.white : Colors.grey,
+ ),
+ onPressed: onToggleCamera,
+ backgroundColor: Colors.black38,
+ ),
+ ),
+ ),
+ SpeedDial(
+ heroTag: "Options",
+ icon: Icons.more_vert,
+ activeIcon: Icons.close,
+ backgroundColor: Colors.black38,
+ switchLabelPosition: true,
+ overlayColor: Colors.black,
+ elevation: 0,
+ overlayOpacity: 0.5,
+ children: [
+ SpeedDialChild(
+ visible: isScreenSharingButtonVisible,
+ child: Icon(
+ isScreenSharingEnabled
+ ? Icons.stop_screen_share
+ : Icons.screen_share,
+ color: Colors.white,
+ ),
+ backgroundColor: Colors.black38,
+ foregroundColor: Colors.white,
+ label:
+ '${isScreenSharingEnabled ? 'Stop' : 'Start'} Screen Sharing',
+ onTap: onToggleScreenSharing,
+ ),
+ SpeedDialChild(
+ visible: !(kIsWeb &&
+ (Browser().browserAgent == BrowserAgent.Safari ||
+ Browser().browserAgent == BrowserAgent.Firefox)),
+ child: Icon(
+ kIsWeb || WebRTC.platformIsDesktop
+ ? Icons.surround_sound
+ : isSpeakerEnabled
+ ? Icons.volume_up
+ : Icons.volume_off,
+ color: isSpeakerEnabled ? Colors.white : Colors.grey,
+ ),
+ backgroundColor: Colors.black38,
+ foregroundColor: Colors.white,
+ label:
+ 'Switch ${kIsWeb || WebRTC.platformIsDesktop ? 'Audio output' : 'Speakerphone'}',
+ onTap: onSwitchSpeaker,
+ ),
+ SpeedDialChild(
+ visible: kIsWeb || WebRTC.platformIsDesktop,
+ child: Icon(
+ Icons.record_voice_over,
+ color: Colors.white,
+ ),
+ backgroundColor: Colors.black38,
+ foregroundColor: Colors.white,
+ label: 'Switch Audio Input device',
+ onTap: onSwitchAudioInput,
+ ),
+ SpeedDialChild(
+ visible: isSwitchCameraButtonVisible,
+ child: Icon(
+ Icons.cameraswitch,
+ color: isCameraEnabled ? Colors.white : Colors.grey,
+ ),
+ backgroundColor: Colors.black38,
+ foregroundColor: Colors.white,
+ label: 'Switch Camera',
+ onTap: onSwitchCamera,
+ ),
+ ],
+ ),
+ Expanded(
+ child: SizedBox(),
+ flex: 1,
+ ),
+ Padding(
+ padding: EdgeInsets.only(left: 0),
+ child: FloatingActionButton(
+ child: Icon(
+ Icons.call_end,
+ color: Colors.white,
+ ),
+ backgroundColor: Colors.red,
+ onPressed: onEndCall,
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/conf_call_sample/lib/src/widgets/call_info_widget.dart b/conf_call_sample/lib/src/widgets/call_info_widget.dart
new file mode 100644
index 0000000..21d2686
--- /dev/null
+++ b/conf_call_sample/lib/src/widgets/call_info_widget.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+
+import '../utils/duration_timer.dart';
+import '../utils/string_utils.dart';
+
+class CallInfo extends StatelessWidget {
+ final String callName;
+ final String callStatus;
+ final DurationTimer callTimer;
+
+ CallInfo(this.callName, this.callStatus, this.callTimer);
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ Text(
+ callName,
+ style: TextStyle(
+ fontSize: 24,
+ color: Colors.white,
+ decoration: TextDecoration.none,
+ shadows: [
+ Shadow(
+ color: Colors.grey.shade900,
+ offset: Offset(2, 1),
+ blurRadius: 12,
+ ),
+ ],
+ ),
+ ),
+ Text(
+ callStatus,
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.white,
+ decoration: TextDecoration.none,
+ shadows: [
+ Shadow(
+ color: Colors.grey.shade900,
+ offset: Offset(2, 1),
+ blurRadius: 12,
+ ),
+ ],
+ ),
+ ),
+ StreamBuilder(
+ stream: callTimer.durationStream,
+ builder: (context, snapshot) {
+ return Text(
+ snapshot.hasData ? formatHHMMSS(snapshot.data!) : '00:00',
+ style: TextStyle(
+ fontSize: 14,
+ color: Colors.white,
+ decoration: TextDecoration.none,
+ shadows: [
+ Shadow(
+ color: Colors.grey.shade900,
+ offset: Offset(2, 1),
+ blurRadius: 12,
+ ),
+ ],
+ ),
+ );
+ })
+ ],
+ );
+ }
+}
diff --git a/conf_call_sample/lib/src/widgets/grid_view_call_widget.dart b/conf_call_sample/lib/src/widgets/grid_view_call_widget.dart
new file mode 100644
index 0000000..5a11636
--- /dev/null
+++ b/conf_call_sample/lib/src/widgets/grid_view_call_widget.dart
@@ -0,0 +1,156 @@
+import 'package:flutter/material.dart';
+
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+import '../utils/media_utils.dart';
+import 'minor_video_widget.dart';
+
+class GridViewLayout extends StatefulWidget {
+ final MapEntry? primaryRenderer;
+ final Map minorRenderers;
+ final int currentUserId;
+ final List participants;
+ final bool isFrontCameraUsed;
+ final bool isScreenSharingEnabled;
+ final Map> participantsMediaConfigs;
+ final Function(MapEntry? primaryRenderer,
+ Map minorRenderers) onRenderersChanged;
+ final CubeStatsReportsManager statsReportsManager;
+
+ GridViewLayout({
+ super.key,
+ required this.currentUserId,
+ required this.participants,
+ required this.primaryRenderer,
+ required this.minorRenderers,
+ required this.isFrontCameraUsed,
+ required this.isScreenSharingEnabled,
+ required this.participantsMediaConfigs,
+ required this.onRenderersChanged,
+ required this.statsReportsManager,
+ });
+
+ @override
+ State createState() {
+ return _GridViewLayoutState(
+ primaryRenderer: primaryRenderer,
+ minorRenderers: minorRenderers,
+ );
+ }
+}
+
+class _GridViewLayoutState extends State {
+ static final String TAG = 'GridViewLayout';
+
+ MapEntry? _primaryRenderer;
+ Map _minorRenderers;
+
+ _GridViewLayoutState({
+ required MapEntry? primaryRenderer,
+ required Map minorRenderers,
+ }) : _primaryRenderer = primaryRenderer,
+ _minorRenderers = minorRenderers;
+
+ @override
+ void didUpdateWidget(GridViewLayout oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ log("[didUpdateWidget]", TAG);
+ _primaryRenderer = widget.primaryRenderer;
+ _minorRenderers = widget.minorRenderers;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var orientation = MediaQuery.of(context).orientation;
+ return Container(
+ margin: MediaQuery.of(context).padding,
+ child: GridView(
+ gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
+ crossAxisCount: orientation == Orientation.portrait ? 2 : 4,
+ crossAxisSpacing: 0,
+ mainAxisSpacing: 0,
+ childAspectRatio: 4 / 3,
+ ),
+ padding: EdgeInsets.all(4),
+ scrollDirection: Axis.vertical,
+ children: _buildGridItems(orientation),
+ ),
+ );
+ }
+
+ List _buildGridItems(Orientation orientation) {
+ Map allRenderers =
+ Map.fromEntries([..._minorRenderers.entries]);
+ if (_primaryRenderer != null) {
+ allRenderers.addEntries([_primaryRenderer!]);
+ }
+ var itemHeight;
+ var itemWidth;
+
+ if (orientation == Orientation.portrait) {
+ itemWidth = MediaQuery.of(context).size.width * 0.95 / 2;
+ itemHeight = itemWidth / 4 * 3;
+ } else {
+ itemWidth = MediaQuery.of(context).size.width * 0.95 / 2;
+ itemHeight = itemWidth / 4 * 3;
+ }
+
+ return buildItems(allRenderers, itemWidth, itemHeight);
+ }
+
+ List buildItems(Map renderers,
+ double itemWidth, double itemHeight) {
+ var videoItems = [];
+
+ renderers.forEach(
+ (key, value) {
+ if ((value.srcObject?.getVideoTracks().isNotEmpty ?? false) &&
+ isUserCameraEnabled(key, widget.participantsMediaConfigs,
+ defaultValue: true)) {
+ videoItems.add(
+ StreamBuilder(
+ stream: widget.statsReportsManager.micLevelStream
+ .where((event) => event.userId == key),
+ builder: (context, snapshot) {
+ var defaultBorderWidth = 4.0;
+ var width = !snapshot.hasData
+ ? 0
+ : snapshot.data!.micLevel * defaultBorderWidth;
+
+ return Container(
+ margin: EdgeInsets.all(defaultBorderWidth),
+ decoration: ShapeDecoration(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12.0),
+ side: BorderSide(
+ width: width.toDouble(),
+ color: Colors.green,
+ strokeAlign: 1.0),
+ ),
+ ),
+ child: MinorVideo(
+ width: itemWidth,
+ height: itemHeight,
+ renderer: value,
+ mirror: key == widget.currentUserId &&
+ widget.isFrontCameraUsed &&
+ !widget.isScreenSharingEnabled,
+ name: key == widget.currentUserId
+ ? 'Me'
+ : widget.participants
+ .where((user) => user.id == key)
+ .first
+ .fullName ??
+ 'Unknown',
+ ),
+ );
+ },
+ ),
+ );
+ }
+ },
+ );
+
+ return videoItems;
+ }
+}
diff --git a/conf_call_sample/lib/src/widgets/minor_video_widget.dart b/conf_call_sample/lib/src/widgets/minor_video_widget.dart
new file mode 100644
index 0000000..a232388
--- /dev/null
+++ b/conf_call_sample/lib/src/widgets/minor_video_widget.dart
@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+class MinorVideo extends StatelessWidget {
+ final double width;
+ final double height;
+ final RTCVideoRenderer renderer;
+ final RTCVideoViewObjectFit objectFit;
+ final bool mirror;
+ final String? name;
+ final Function()? onTap;
+ final Function(DragUpdateDetails details)? onPanUpdate;
+ final Function(DragEndDetails details)? onPanEnd;
+
+ MinorVideo({
+ super.key,
+ required this.width,
+ required this.height,
+ required this.renderer,
+ required this.mirror,
+ this.name,
+ this.objectFit = RTCVideoViewObjectFit.RTCVideoViewObjectFitCover,
+ this.onTap,
+ this.onPanUpdate,
+ this.onPanEnd,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return GestureDetector(
+ onPanUpdate: onPanUpdate,
+ onPanEnd: onPanEnd,
+ onTap: onTap,
+ child: AbsorbPointer(
+ child: SizedBox(
+ width: width,
+ height: height,
+ child: Stack(
+ children: [
+ ClipRRect(
+ borderRadius: BorderRadius.circular(12.0),
+ child: RTCVideoView(
+ renderer,
+ objectFit: objectFit,
+ mirror: mirror,
+ ),
+ ),
+ Visibility(
+ visible: name != null && name!.isNotEmpty,
+ child: Align(
+ alignment: Alignment.bottomCenter,
+ child: Container(
+ margin: EdgeInsets.only(bottom: 8),
+ child: Text(
+ name ?? 'Unknown',
+ style: TextStyle(
+ color: Colors.white,
+ shadows: [
+ Shadow(
+ color: Colors.black,
+ offset: Offset(2, 1),
+ blurRadius: 8,
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/conf_call_sample/lib/src/widgets/primary_video_widget.dart b/conf_call_sample/lib/src/widgets/primary_video_widget.dart
new file mode 100644
index 0000000..897fcde
--- /dev/null
+++ b/conf_call_sample/lib/src/widgets/primary_video_widget.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/material.dart';
+
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+class PrimaryVideo extends StatelessWidget {
+ final RTCVideoRenderer renderer;
+ final RTCVideoViewObjectFit objectFit;
+ final bool mirror;
+ final Function()? onDoubleTap;
+
+ PrimaryVideo({
+ super.key,
+ required this.renderer,
+ required this.objectFit,
+ required this.mirror,
+ this.onDoubleTap,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Stack(children: [
+ GestureDetector(
+ onDoubleTap: onDoubleTap,
+ child: RTCVideoView(
+ renderer,
+ objectFit: objectFit,
+ mirror: mirror,
+ ),
+ ),
+ ]);
+ }
+}
diff --git a/conf_call_sample/lib/src/widgets/private_call_widget.dart b/conf_call_sample/lib/src/widgets/private_call_widget.dart
new file mode 100644
index 0000000..03ea745
--- /dev/null
+++ b/conf_call_sample/lib/src/widgets/private_call_widget.dart
@@ -0,0 +1,344 @@
+import 'package:flutter/material.dart';
+
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+import '../utils/duration_timer.dart';
+import '../utils/media_utils.dart';
+import 'call_info_widget.dart';
+import 'minor_video_widget.dart';
+import 'primary_video_widget.dart';
+
+class PrivateCallLayout extends StatefulWidget {
+ final MapEntry? primaryRenderer;
+ final Map minorRenderers;
+ final RTCVideoViewObjectFit primaryVideoFit;
+ final Function(RTCVideoViewObjectFit newObjectFit)? onPrimaryVideoFitChanged;
+ final int currentUserId;
+ final String callName;
+ final String callStatus;
+ final DurationTimer callTimer;
+ final bool isFrontCameraUsed;
+ final bool isScreenSharingEnabled;
+ final Map> participantsMediaConfigs;
+ final WidgetPosition minorWidgetInitialPosition;
+ final Function(WidgetPosition newPosition)? onMinorVideoPositionChanged;
+ final Function(MapEntry? primaryRenderer,
+ Map minorRenderers) onRenderersChanged;
+
+ PrivateCallLayout({
+ super.key,
+ required this.currentUserId,
+ required this.primaryRenderer,
+ required this.primaryVideoFit,
+ this.onPrimaryVideoFitChanged,
+ required this.minorRenderers,
+ required this.callName,
+ required this.callStatus,
+ required this.callTimer,
+ required this.minorWidgetInitialPosition,
+ required this.isFrontCameraUsed,
+ required this.isScreenSharingEnabled,
+ required this.participantsMediaConfigs,
+ this.onMinorVideoPositionChanged,
+ required this.onRenderersChanged,
+ });
+
+ @override
+ State createState() {
+ return _PrivateCallLayoutState(
+ primaryRenderer: primaryRenderer,
+ minorRenderers: minorRenderers,
+ primaryVideoFit: primaryVideoFit,
+ minorWidgetInitialPosition: minorWidgetInitialPosition,
+ );
+ }
+}
+
+class _PrivateCallLayoutState extends State {
+ static final String TAG = 'PrivateCallLayout';
+
+ MapEntry? _primaryRenderer;
+ Map _minorRenderers;
+ RTCVideoViewObjectFit _primaryVideoFit;
+ bool _isPrimaryUserForciblySelected = false;
+ WidgetPosition _minorWidgetInitialPosition;
+ Offset _minorWidgetOffset = Offset(0, 0);
+ bool _isWidgetMoving = false;
+
+ _PrivateCallLayoutState({
+ required MapEntry? primaryRenderer,
+ required Map minorRenderers,
+ required RTCVideoViewObjectFit primaryVideoFit,
+ required WidgetPosition minorWidgetInitialPosition,
+ }) : _primaryRenderer = primaryRenderer,
+ _minorRenderers = minorRenderers,
+ _primaryVideoFit = primaryVideoFit,
+ _minorWidgetInitialPosition = minorWidgetInitialPosition;
+
+ @override
+ void didUpdateWidget(PrivateCallLayout oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ log("[didUpdateWidget]", TAG);
+ _primaryRenderer = widget.primaryRenderer;
+ _minorRenderers = widget.minorRenderers;
+ _primaryVideoFit = widget.primaryVideoFit;
+ _minorWidgetInitialPosition = widget.minorWidgetInitialPosition;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var orientation = MediaQuery.of(context).orientation;
+ log("[build]", TAG);
+
+ List children = [];
+
+ var primaryVideo = buildPrimaryVideoWidget();
+ if (primaryVideo != null) {
+ children.add(primaryVideo);
+ }
+
+ children.add(buildCallInfoWidget());
+
+ var minorVideo = buildMinorVideoWidget(orientation);
+ if (minorVideo != null) {
+ children.add(minorVideo);
+ }
+
+ return Stack(children: children);
+ }
+
+ Widget? buildPrimaryVideoWidget() {
+ Widget? createPrimaryVideoWidget() {
+ if (canShowVideo(_primaryRenderer?.key, _primaryRenderer?.value.srcObject,
+ widget.participantsMediaConfigs)) {
+ return PrimaryVideo(
+ renderer: _primaryRenderer!.value,
+ objectFit: _primaryVideoFit,
+ mirror: _primaryRenderer?.key == widget.currentUserId &&
+ widget.isFrontCameraUsed &&
+ !widget.isScreenSharingEnabled,
+ onDoubleTap: () {
+ setState(() {
+ _primaryVideoFit = _primaryVideoFit ==
+ RTCVideoViewObjectFit.RTCVideoViewObjectFitCover
+ ? RTCVideoViewObjectFit.RTCVideoViewObjectFitContain
+ : RTCVideoViewObjectFit.RTCVideoViewObjectFitCover;
+ widget.onPrimaryVideoFitChanged?.call(_primaryVideoFit);
+ });
+ },
+ );
+ }
+
+ return null;
+ }
+
+ var primaryVideoWidget;
+
+ var minorUserWithEnabledVideo = getUserWithEnabledVideo(
+ _minorRenderers, widget.currentUserId, widget.participantsMediaConfigs);
+
+ if ((_primaryRenderer?.key != widget.currentUserId ||
+ (_primaryRenderer?.key == widget.currentUserId &&
+ (_isPrimaryUserForciblySelected ||
+ minorUserWithEnabledVideo == null))) &&
+ canShowVideo(_primaryRenderer?.key, _primaryRenderer?.value.srcObject,
+ widget.participantsMediaConfigs)) {
+ primaryVideoWidget = createPrimaryVideoWidget();
+ } else if (minorUserWithEnabledVideo != null) {
+ updatePrimaryUser(
+ minorUserWithEnabledVideo,
+ true,
+ widget.currentUserId,
+ _primaryRenderer,
+ _minorRenderers,
+ widget.participantsMediaConfigs,
+ onRenderersUpdated: (newPrimaryRenderer, newMinorRenderers) {
+ widget.onRenderersChanged.call(newPrimaryRenderer, newMinorRenderers);
+ _primaryRenderer = newPrimaryRenderer;
+ _minorRenderers = newMinorRenderers;
+
+ _isPrimaryUserForciblySelected = false;
+ primaryVideoWidget = createPrimaryVideoWidget();
+ },
+ );
+ }
+
+ return primaryVideoWidget;
+ }
+
+ Widget buildCallInfoWidget() {
+ return Align(
+ alignment: Alignment.topCenter,
+ child: Container(
+ margin: EdgeInsets.only(
+ top: MediaQuery.of(context).padding.top + 48,
+ ),
+ child: CallInfo(
+ widget.callName,
+ widget.callStatus,
+ widget.callTimer,
+ ),
+ ),
+ );
+ }
+
+ Widget? buildMinorVideoWidget(Orientation orientation) {
+ var width = calculateMinorVideoViewWidth(context, orientation);
+ var height = calculateMinorVideoViewHeight(context, orientation);
+
+ var videoItems = [];
+
+ _minorRenderers.forEach(
+ (key, value) {
+ if ((value.srcObject?.getVideoTracks().isNotEmpty ?? false) &&
+ isUserCameraEnabled(key, widget.participantsMediaConfigs,
+ defaultValue: true)) {
+ videoItems.add(
+ MinorVideo(
+ width: width,
+ height: height,
+ renderer: value,
+ mirror: key == widget.currentUserId &&
+ widget.isFrontCameraUsed &&
+ !widget.isScreenSharingEnabled,
+ onPanUpdate: (details) =>
+ _onPanUpdate(context, details, _minorWidgetOffset),
+ onPanEnd: (details) => _onPanEnd(context, details),
+ onTap: () => setState(
+ () {
+ log("[onTap] userId: $key", TAG);
+ updatePrimaryUser(
+ key,
+ true,
+ widget.currentUserId,
+ _primaryRenderer,
+ _minorRenderers,
+ widget.participantsMediaConfigs,
+ onRenderersUpdated:
+ (newPrimaryRenderer, newMinorRenderers) {
+ widget.onRenderersChanged
+ .call(newPrimaryRenderer, newMinorRenderers);
+ _primaryRenderer = newPrimaryRenderer;
+ _minorRenderers = newMinorRenderers;
+ },
+ );
+ _isPrimaryUserForciblySelected = true;
+ },
+ ),
+ ),
+ );
+ }
+ },
+ );
+
+ if (videoItems.isEmpty) return null;
+
+ var minorItem = videoItems.firstOrNull;
+
+ if (minorItem != null) {
+ var widgetOffset =
+ getOffsetForPosition(context, _minorWidgetInitialPosition);
+
+ if (_isWidgetMoving) {
+ widgetOffset = _minorWidgetOffset;
+ }
+
+ return Positioned(
+ top: widgetOffset.dy - height / 2,
+ left: widgetOffset.dx - width / 2,
+ child: minorItem,
+ );
+ }
+
+ return minorItem;
+ }
+
+ void _onPanUpdate(
+ BuildContext context, DragUpdateDetails details, Offset offset) {
+ log('_onPanUpdate', TAG);
+
+ setState(() {
+ _isWidgetMoving = true;
+ _minorWidgetOffset = details.globalPosition;
+ });
+ }
+
+ void _onPanEnd(BuildContext context, DragEndDetails details) {
+ log('_onPanEnd', TAG);
+
+ setState(() {
+ _isWidgetMoving = false;
+ _minorWidgetInitialPosition =
+ calculateMinorVideoViewPosition(context, _minorWidgetOffset);
+ widget.onMinorVideoPositionChanged?.call(_minorWidgetInitialPosition);
+ });
+ }
+}
+
+double calculateMinorVideoViewWidth(
+ BuildContext context, Orientation orientation) {
+ return orientation == Orientation.portrait
+ ? MediaQuery.of(context).size.width / 3
+ : MediaQuery.of(context).size.width / 4;
+}
+
+double calculateMinorVideoViewHeight(
+ BuildContext context, Orientation orientation) {
+ return orientation == Orientation.portrait
+ ? MediaQuery.of(context).size.height / 4
+ : MediaQuery.of(context).size.height / 2.5;
+}
+
+WidgetPosition calculateMinorVideoViewPosition(
+ BuildContext context, Offset initialPosition) {
+ var isRight = false;
+ if (initialPosition.dx > MediaQuery.of(context).size.width / 2) {
+ isRight = true;
+ }
+
+ var isBottom = false;
+ if (initialPosition.dy > MediaQuery.of(context).size.height / 2) {
+ isBottom = true;
+ }
+
+ var position = WidgetPosition.topRight;
+
+ if (isRight && isBottom) {
+ position = WidgetPosition.bottomRight;
+ } else if (!isRight && isBottom) {
+ position = WidgetPosition.bottomLeft;
+ } else if (!isRight && !isBottom) {
+ position = WidgetPosition.topLeft;
+ }
+
+ return position;
+}
+
+Offset getOffsetForPosition(BuildContext context, WidgetPosition position) {
+ var orientation = MediaQuery.of(context).orientation;
+
+ var width = calculateMinorVideoViewWidth(context, orientation);
+ var height = calculateMinorVideoViewHeight(context, orientation);
+
+ var dxPosition = 0.0;
+ if (position == WidgetPosition.topRight ||
+ position == WidgetPosition.bottomRight) {
+ dxPosition = MediaQuery.of(context).size.width -
+ (width / 2 + MediaQuery.of(context).padding.right + 10);
+ } else {
+ dxPosition = width / 2 + MediaQuery.of(context).padding.left + 10;
+ }
+
+ var dyPosition = 0.0;
+ if (position == WidgetPosition.bottomRight ||
+ position == WidgetPosition.bottomLeft) {
+ dyPosition = MediaQuery.of(context).size.height -
+ (height / 2 + MediaQuery.of(context).padding.bottom + 10);
+ } else {
+ dyPosition = height / 2 + MediaQuery.of(context).padding.top + 10;
+ }
+
+ return Offset(dxPosition, dyPosition);
+}
+
+enum WidgetPosition { topLeft, topRight, bottomLeft, bottomRight }
diff --git a/conf_call_sample/lib/src/widgets/speaker_view_call_widget.dart b/conf_call_sample/lib/src/widgets/speaker_view_call_widget.dart
new file mode 100644
index 0000000..c056ae4
--- /dev/null
+++ b/conf_call_sample/lib/src/widgets/speaker_view_call_widget.dart
@@ -0,0 +1,334 @@
+import 'package:flutter/material.dart';
+
+import 'package:connectycube_sdk/connectycube_sdk.dart';
+
+import '../utils/duration_timer.dart';
+import '../utils/media_utils.dart';
+import '../managers/speakers_manager.dart';
+import 'call_info_widget.dart';
+import 'minor_video_widget.dart';
+import 'primary_video_widget.dart';
+
+class SpeakerViewLayout extends StatefulWidget {
+ final MapEntry? primaryRenderer;
+ final Map minorRenderers;
+ final RTCVideoViewObjectFit primaryVideoFit;
+ final Function(RTCVideoViewObjectFit newObjectFit)? onPrimaryVideoFitChanged;
+ final int currentUserId;
+ final List participants;
+ final String callName;
+ final String callStatus;
+ final DurationTimer callTimer;
+ final bool isFrontCameraUsed;
+ final bool isScreenSharingEnabled;
+ final Map> participantsMediaConfigs;
+ final Function(MapEntry? primaryRenderer,
+ Map minorRenderers) onRenderersChanged;
+ final CubeStatsReportsManager statsReportsManager;
+
+ SpeakerViewLayout({
+ super.key,
+ required this.currentUserId,
+ required this.participants,
+ required this.primaryRenderer,
+ required this.primaryVideoFit,
+ this.onPrimaryVideoFitChanged,
+ required this.minorRenderers,
+ required this.callName,
+ required this.callStatus,
+ required this.callTimer,
+ required this.isFrontCameraUsed,
+ required this.isScreenSharingEnabled,
+ required this.participantsMediaConfigs,
+ required this.onRenderersChanged,
+ required this.statsReportsManager,
+ });
+
+ @override
+ State createState() {
+ return _SpeakerViewLayoutState(
+ primaryRenderer: primaryRenderer,
+ minorRenderers: minorRenderers,
+ primaryVideoFit: primaryVideoFit,
+ );
+ }
+}
+
+class _SpeakerViewLayoutState extends State {
+ static final String TAG = 'SpeakerViewLayout';
+ final SpeakersManager _speakersManager = SpeakersManager();
+
+ MapEntry? _primaryRenderer;
+ Map _minorRenderers;
+ RTCVideoViewObjectFit _primaryVideoFit;
+ bool _isPrimaryUserForciblySelected = false;
+
+ _SpeakerViewLayoutState({
+ required MapEntry? primaryRenderer,
+ required Map minorRenderers,
+ required RTCVideoViewObjectFit primaryVideoFit,
+ }) : _primaryRenderer = primaryRenderer,
+ _minorRenderers = minorRenderers,
+ _primaryVideoFit = primaryVideoFit;
+
+ @override
+ void initState() {
+ _speakersManager.init(widget.statsReportsManager, _onSpeakerChanged);
+ }
+
+ @override
+ void didUpdateWidget(SpeakerViewLayout oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ log("[didUpdateWidget]", TAG);
+ _primaryRenderer = widget.primaryRenderer;
+ _minorRenderers = widget.minorRenderers;
+ _primaryVideoFit = widget.primaryVideoFit;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ var orientation = MediaQuery.of(context).orientation;
+ return Center(
+ child: Container(
+ child: Stack(children: [
+ orientation == Orientation.portrait
+ ? Column(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: renderSpeakerModeViews(orientation))
+ : Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: renderSpeakerModeViews(orientation)),
+ buildCallInfoWidget(),
+ ]),
+ ));
+ }
+
+ void _onSpeakerChanged(int userId) {
+ log('[_onSpeakerChanged] userId: $userId, currentUserId: ${widget.currentUserId}',
+ TAG);
+ if (userId == widget.currentUserId) return;
+
+ if (canShowVideo(userId, _minorRenderers[userId]?.srcObject,
+ widget.participantsMediaConfigs)) {
+ setState(() {
+ updatePrimaryUser(
+ userId,
+ false,
+ widget.currentUserId,
+ _primaryRenderer,
+ _minorRenderers,
+ widget.participantsMediaConfigs,
+ onRenderersUpdated: (newPrimaryRenderer, newMinorRenderers) {
+ widget.onRenderersChanged
+ .call(newPrimaryRenderer, newMinorRenderers);
+ _primaryRenderer = newPrimaryRenderer;
+ _minorRenderers = newMinorRenderers;
+ _isPrimaryUserForciblySelected = false;
+ },
+ );
+ });
+ }
+ }
+
+ List renderSpeakerModeViews(Orientation orientation) {
+ log("[renderSpeakerModeViews]", TAG);
+ List streamsExpanded = [];
+
+ var primaryVideo = buildPrimaryVideoWidget();
+ if (primaryVideo != null) {
+ streamsExpanded.add(primaryVideo);
+ }
+
+ var minorItems = buildMinorVideoItems(orientation);
+ if (minorItems != null) {
+ streamsExpanded.add(minorItems);
+ }
+
+ return streamsExpanded;
+ }
+
+ buildPrimaryVideoWidget() {
+ Widget? createPrimaryVideoView() {
+ var primaryVideo;
+ if (canShowVideo(_primaryRenderer?.key, _primaryRenderer?.value.srcObject,
+ widget.participantsMediaConfigs)) {
+ primaryVideo = Expanded(
+ flex: 3,
+ child: PrimaryVideo(
+ renderer: _primaryRenderer!.value,
+ objectFit: widget.primaryVideoFit,
+ mirror: _primaryRenderer!.key == widget.currentUserId &&
+ widget.isFrontCameraUsed &&
+ !widget.isScreenSharingEnabled,
+ onDoubleTap: () {
+ setState(() {
+ _primaryVideoFit = _primaryVideoFit ==
+ RTCVideoViewObjectFit.RTCVideoViewObjectFitCover
+ ? RTCVideoViewObjectFit.RTCVideoViewObjectFitContain
+ : RTCVideoViewObjectFit.RTCVideoViewObjectFitCover;
+ widget.onPrimaryVideoFitChanged?.call(_primaryVideoFit);
+ });
+ },
+ ),
+ );
+ }
+
+ return primaryVideo;
+ }
+
+ var primaryVideoWidget;
+
+ var minorUserWithEnabledVideo = getUserWithEnabledVideo(
+ _minorRenderers, widget.currentUserId, widget.participantsMediaConfigs);
+
+ if ((_primaryRenderer?.key != widget.currentUserId ||
+ (_primaryRenderer?.key == widget.currentUserId &&
+ (_isPrimaryUserForciblySelected ||
+ minorUserWithEnabledVideo == null))) &&
+ canShowVideo(_primaryRenderer?.key, _primaryRenderer?.value.srcObject,
+ widget.participantsMediaConfigs)) {
+ primaryVideoWidget = createPrimaryVideoView();
+ } else if (minorUserWithEnabledVideo != null) {
+ updatePrimaryUser(
+ minorUserWithEnabledVideo,
+ true,
+ widget.currentUserId,
+ _primaryRenderer,
+ _minorRenderers,
+ widget.participantsMediaConfigs,
+ onRenderersUpdated: (newPrimaryRenderer, newMinorRenderers) {
+ widget.onRenderersChanged.call(newPrimaryRenderer, newMinorRenderers);
+ _primaryRenderer = newPrimaryRenderer;
+ _minorRenderers = newMinorRenderers;
+ },
+ );
+ _isPrimaryUserForciblySelected = false;
+ primaryVideoWidget = createPrimaryVideoView();
+ }
+
+ return primaryVideoWidget;
+ }
+
+ Widget? buildMinorVideoItems(Orientation orientation) {
+ var itemHeight;
+ var itemWidth;
+
+ if (orientation == Orientation.portrait) {
+ itemHeight = MediaQuery.of(context).size.height / 3 * 0.8;
+ itemWidth = itemHeight / 3 * 4;
+ } else {
+ itemWidth = MediaQuery.of(context).size.width / 3 * 0.8;
+ itemHeight = itemWidth / 4 * 3;
+ }
+
+ var videoItems = [];
+
+ _minorRenderers.forEach(
+ (key, value) {
+ if ((value.srcObject?.getVideoTracks().isNotEmpty ?? false) &&
+ isUserCameraEnabled(key, widget.participantsMediaConfigs,
+ defaultValue: true)) {
+ videoItems.add(
+ StreamBuilder(
+ stream: widget.statsReportsManager.micLevelStream
+ .where((event) => event.userId == key),
+ builder: (context, snapshot) {
+ var defaultBorderWidth = 4.0;
+ var width = !snapshot.hasData
+ ? 0
+ : snapshot.data!.micLevel * defaultBorderWidth;
+
+ return Padding(
+ padding: EdgeInsets.all(2),
+ child: Container(
+ margin: EdgeInsets.all(defaultBorderWidth),
+ decoration: ShapeDecoration(
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(12.0),
+ side: BorderSide(
+ width: width.toDouble(),
+ color: Colors.green,
+ strokeAlign: 1.0),
+ ),
+ ),
+ child: MinorVideo(
+ width: itemWidth,
+ height: itemHeight,
+ renderer: value,
+ mirror: key == widget.currentUserId &&
+ widget.isFrontCameraUsed &&
+ !widget.isScreenSharingEnabled,
+ name: key == widget.currentUserId
+ ? 'Me'
+ : widget.participants
+ .where((user) => user.id == key)
+ .first
+ .fullName ??
+ 'Unknown',
+ onTap: () => setState(
+ () {
+ log("[onTap] userId: $key", TAG);
+ updatePrimaryUser(
+ key,
+ true,
+ widget.currentUserId,
+ _primaryRenderer,
+ _minorRenderers,
+ widget.participantsMediaConfigs,
+ onRenderersUpdated:
+ (newPrimaryRenderer, newMinorRenderers) {
+ widget.onRenderersChanged.call(
+ newPrimaryRenderer, newMinorRenderers);
+ _primaryRenderer = newPrimaryRenderer;
+ _minorRenderers = newMinorRenderers;
+ },
+ );
+ _isPrimaryUserForciblySelected = true;
+ },
+ ),
+ ),
+ ));
+ },
+ ),
+ );
+ }
+ },
+ );
+
+ var minorVideoItems;
+
+ if (videoItems.isNotEmpty) {
+ var membersList = Expanded(
+ flex: 1,
+ child: ListView(
+ scrollDirection: orientation == Orientation.landscape
+ ? Axis.vertical
+ : Axis.horizontal,
+ children: videoItems,
+ ),
+ );
+
+ minorVideoItems = membersList;
+ }
+
+ return minorVideoItems;
+ }
+
+ Widget buildCallInfoWidget() {
+ return Align(
+ alignment: Alignment.topCenter,
+ child: Container(
+ margin: EdgeInsets.only(
+ top: MediaQuery.of(context).padding.top + 48,
+ ),
+ child: CallInfo(widget.callName, widget.callStatus, widget.callTimer),
+ ),
+ );
+ }
+
+ @override
+ void dispose() {
+ _speakersManager.dispose();
+ super.dispose();
+ }
+}
diff --git a/conf_call_sample/macos/Flutter/GeneratedPluginRegistrant.swift b/conf_call_sample/macos/Flutter/GeneratedPluginRegistrant.swift
index e559391..e399ce3 100644
--- a/conf_call_sample/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/conf_call_sample/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -5,12 +5,20 @@
import FlutterMacOS
import Foundation
+import assets_audio_player
+import assets_audio_player_web
import device_info_plus
import flutter_webrtc
+import package_info_plus
import path_provider_foundation
+import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
+ AssetsAudioPlayerPlugin.register(with: registry.registrar(forPlugin: "AssetsAudioPlayerPlugin"))
+ AssetsAudioPlayerWebPlugin.register(with: registry.registrar(forPlugin: "AssetsAudioPlayerWebPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
+ FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
+ SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}
diff --git a/conf_call_sample/macos/Podfile.lock b/conf_call_sample/macos/Podfile.lock
index 2875101..46eda99 100644
--- a/conf_call_sample/macos/Podfile.lock
+++ b/conf_call_sample/macos/Podfile.lock
@@ -1,20 +1,20 @@
PODS:
- device_info_plus (0.0.1):
- FlutterMacOS
- - flutter_webrtc (0.9.22):
+ - flutter_webrtc (0.9.31):
- FlutterMacOS
- - WebRTC-SDK (= 104.5112.12)
+ - WebRTC-SDK (= 104.5112.17)
- FlutterMacOS (1.0.0)
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- - WebRTC-SDK (104.5112.12)
+ - WebRTC-SDK (104.5112.17)
DEPENDENCIES:
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- flutter_webrtc (from `Flutter/ephemeral/.symlinks/plugins/flutter_webrtc/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos`)
+ - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`)
SPEC REPOS:
trunk:
@@ -28,14 +28,14 @@ EXTERNAL SOURCES:
FlutterMacOS:
:path: Flutter/ephemeral
path_provider_foundation:
- :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/macos
+ :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin
SPEC CHECKSUMS:
device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f
- flutter_webrtc: 0e2af3876ed28d4599c06734c771c2526309356b
+ flutter_webrtc: 9903b25f3648e335a40952058b14225e7b0df88d
FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24
- path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9
- WebRTC-SDK: 6469c304549f7187e7e67119c8b3184f9ec6b7f6
+ path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8
+ WebRTC-SDK: 082ae4893212534a779ca233f19a9df8efd5f3bd
PODFILE CHECKSUM: 0095aabf5a2ba4b7b30669b8afaa68ef378ace64
diff --git a/conf_call_sample/pubspec.yaml b/conf_call_sample/pubspec.yaml
index b574f98..023f8e1 100644
--- a/conf_call_sample/pubspec.yaml
+++ b/conf_call_sample/pubspec.yaml
@@ -1,21 +1,31 @@
name: conf_call_sample
description: Conf Calls Sample
-version: 0.3.2
+version: 0.3.6
environment:
- sdk: '>=2.12.0 <3.0.0'
+ sdk: '>=3.0.0 <4.0.0'
dependencies:
flutter:
sdk: flutter
- connectycube_sdk: ^2.5.0
+ assets_audio_player: ^3.1.1
+ connectycube_sdk: ^2.11.0
# connectycube_sdk:
# path: ../../flutter-sdk
+ connectycube_flutter_call_kit: ^2.5.0
+# connectycube_flutter_call_kit:
+# path: ../../connectycube-flutter-call-kit/connectycube_flutter_call_kit
+ device_info_plus: ^9.0.0
flutter_background: ^1.2.0
+ flutter_speed_dial: ^7.0.0
+ package_info_plus: ^5.0.0
+ permission_handler: ^11.0.1
+ shared_preferences: ^2.2.1
+ universal_io: ^2.2.2
+ uuid: ^3.0.7
web_browser_detect: ^2.0.3
- flutter_speed_dial: ^6.2.0
dev_dependencies:
flutter_test:
@@ -23,4 +33,6 @@ dev_dependencies:
flutter:
- uses-material-design: true
\ No newline at end of file
+ uses-material-design: true
+ assets:
+ - assets/audio/
\ No newline at end of file
diff --git a/conf_call_sample/windows/flutter/generated_plugin_registrant.cc b/conf_call_sample/windows/flutter/generated_plugin_registrant.cc
index e8559e4..d5acadb 100644
--- a/conf_call_sample/windows/flutter/generated_plugin_registrant.cc
+++ b/conf_call_sample/windows/flutter/generated_plugin_registrant.cc
@@ -7,8 +7,11 @@
#include "generated_plugin_registrant.h"
#include
+#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
FlutterWebRTCPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FlutterWebRTCPlugin"));
+ PermissionHandlerWindowsPluginRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin"));
}
diff --git a/conf_call_sample/windows/flutter/generated_plugins.cmake b/conf_call_sample/windows/flutter/generated_plugins.cmake
index 3024cac..cb004cd 100644
--- a/conf_call_sample/windows/flutter/generated_plugins.cmake
+++ b/conf_call_sample/windows/flutter/generated_plugins.cmake
@@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
flutter_webrtc
+ permission_handler_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST