diff --git a/README.md b/README.md
index 8d53903..a8ea90f 100644
--- a/README.md
+++ b/README.md
@@ -13,3 +13,4 @@
* New Quickblox Samples:
* [Sample Chat Flutter](https://github.com/QuickBlox/quickblox-flutter-samples/tree/master/chat_sample)
* [Simple-sample](https://github.com/QuickBlox/quickblox-flutter-samples/tree/master/simple_sample)
+ * [Sample Video Call WebRTC](https://github.com/QuickBlox/quickblox-flutter-samples/tree/master/videocall_webrtc_sample)
diff --git a/videocall_webrtc_sample/.gitignore b/videocall_webrtc_sample/.gitignore
new file mode 100644
index 0000000..24476c5
--- /dev/null
+++ b/videocall_webrtc_sample/.gitignore
@@ -0,0 +1,44 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# The .vscode folder contains launch configuration and tasks you configure in
+# VS Code which you may wish to be included in version control, so this line
+# is commented out by default.
+#.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/videocall_webrtc_sample/.metadata b/videocall_webrtc_sample/.metadata
new file mode 100644
index 0000000..8e89d4d
--- /dev/null
+++ b/videocall_webrtc_sample/.metadata
@@ -0,0 +1,45 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled.
+
+version:
+ revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ channel: stable
+
+project_type: app
+
+# Tracks metadata for the flutter migrate command
+migration:
+ platforms:
+ - platform: root
+ create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ - platform: android
+ create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ - platform: ios
+ create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ - platform: linux
+ create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ - platform: macos
+ create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ - platform: web
+ create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ - platform: windows
+ create_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+ base_revision: 9944297138845a94256f1cf37beb88ff9a8e811a
+
+ # User provided section
+
+ # List of Local paths (relative to this file) that should be
+ # ignored by the migrate tool.
+ #
+ # Files that are not part of the templates will be ignored by default.
+ unmanaged_files:
+ - 'lib/main.dart'
+ - 'ios/Runner.xcodeproj/project.pbxproj'
diff --git a/videocall_webrtc_sample/LICENSE.md b/videocall_webrtc_sample/LICENSE.md
new file mode 100644
index 0000000..d58465a
--- /dev/null
+++ b/videocall_webrtc_sample/LICENSE.md
@@ -0,0 +1,19 @@
+Copyright © 2023 QuickBlox
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/videocall_webrtc_sample/README.md b/videocall_webrtc_sample/README.md
new file mode 100644
index 0000000..19688dc
--- /dev/null
+++ b/videocall_webrtc_sample/README.md
@@ -0,0 +1,42 @@
+
QuickBlox Flutter VideoChat Sample
+
+# Overview
+
+This is a code sample for [QuickBlox](http://quickblox.com/) platform.
+It is a great way for developers using QuickBlox platform to learn how to integrate audio and video calling features into your application.
+
+# Features
+
+QuickBlox Flutter VideoChat Sample provides next functionality:
+
+- Authenticate with Quickblox
+- Receive and display users list
+- Make audio calls
+- Make video calls
+
+# Get application credentials
+
+QuickBlox application includes everything that brings messaging right into your application - chat, video calling, users, push notifications, etc. To create a QuickBlox application, follow the steps below:
+
+ 1.Register a new account following [this link](https://admin.quickblox.com/signup). Type in your email and password to sign in. You can also sign in with your Google or Github accounts.
+ 2.Create the app clicking **New app** button.
+ 3.Configure the app. Type in the information about your organization into corresponding fields and click **Add** button.
+ 4.Go to **Dashboard => _YOUR_APP_ => Overview** section and copy your **Application ID**, **Authorization Key**, **Authorization Secret**, and **Account Key**.
+
+# To run the Video Sample
+
+ 1. Clone the repository using the link below:
+
+ git clone https://github.com/QuickBlox/quickblox-flutter-samples.git
+
+ 2. Go to menu **File => Open Project**. (Or "Open an existing Project" if (Android Studio/Visual Studio Code) is just opened)
+ 3. Select a path to the sample.
+ 4. [Get application credentials](#get-application-credentials) and get **Application ID**, **Authorization Key**, **Authorization Secret**, and **Account Key**.
+ 5. Open **main.dart** and paste the credentials into the values of constants.
+
+ const val APPLICATION_ID = ""
+ const val AUTH_KEY = ""
+ const val AUTH_SECRET = ""
+ const val ACCOUNT_KEY = "";
+
+ 6. Run the code sample.
diff --git a/videocall_webrtc_sample/analysis_options.yaml b/videocall_webrtc_sample/analysis_options.yaml
new file mode 100644
index 0000000..d0f4914
--- /dev/null
+++ b/videocall_webrtc_sample/analysis_options.yaml
@@ -0,0 +1,30 @@
+# This file configures the analyzer, which statically analyzes Dart code to
+# check for errors, warnings, and lints.
+#
+# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
+# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
+# invoked from the command line by running `flutter analyze`.
+
+# The following line activates a set of recommended lints for Flutter apps,
+# packages, and plugins designed to encourage good coding practices.
+include: package:flutter_lints/flutter.yaml
+
+linter:
+ # The lint rules applied to this project can be customized in the
+ # section below to disable rules from the `package:flutter_lints/flutter.yaml`
+ # included above or to enable additional rules. A list of all available lints
+ # and their documentation is published at
+ # https://dart-lang.github.io/linter/lints/index.html.
+ #
+ # Instead of disabling a lint rule for the entire project in the
+ # section below, it can also be suppressed for a single line of code
+ # or a specific dart file by using the `// ignore: name_of_lint` and
+ # `// ignore_for_file: name_of_lint` syntax on the line or in the file
+ # producing the lint.
+ rules:
+ constant_identifier_names: false
+ # avoid_print: false # Uncomment to disable the `avoid_print` rule
+ # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
+
+# Additional information about this file can be found at
+# https://dart.dev/guides/language/analysis-options
diff --git a/videocall_webrtc_sample/android/.gitignore b/videocall_webrtc_sample/android/.gitignore
new file mode 100644
index 0000000..6f56801
--- /dev/null
+++ b/videocall_webrtc_sample/android/.gitignore
@@ -0,0 +1,13 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/videocall_webrtc_sample/android/app/build.gradle b/videocall_webrtc_sample/android/app/build.gradle
new file mode 100644
index 0000000..c3d8004
--- /dev/null
+++ b/videocall_webrtc_sample/android/app/build.gradle
@@ -0,0 +1,75 @@
+plugins {
+ id "com.android.application"
+ id "kotlin-android"
+ id "dev.flutter.flutter-gradle-plugin"
+}
+
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+android {
+ compileSdkVersion 34
+ ndkVersion flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility JavaVersion.VERSION_1_8
+ targetCompatibility JavaVersion.VERSION_1_8
+ }
+
+ kotlinOptions {
+ jvmTarget = '1.8'
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId "com.quickblox.videocall_webrtc_sample"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
+ minSdkVersion 26
+ targetSdkVersion 34
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ debug {
+ signingConfig signingConfigs.debug
+ minifyEnabled false
+ shrinkResources false
+ }
+
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {}
diff --git a/videocall_webrtc_sample/android/app/proguard-rules.pro b/videocall_webrtc_sample/android/app/proguard-rules.pro
new file mode 100644
index 0000000..f5ac73c
--- /dev/null
+++ b/videocall_webrtc_sample/android/app/proguard-rules.pro
@@ -0,0 +1,63 @@
+# Add project specific ProGuard rules here.
+# You can control the set of applied configuration files using the
+# proguardFiles setting in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
+
+# Uncomment this to preserve the line number information for
+# debugging stack traces.
+#-keepattributes SourceFile,LineNumberTable
+
+# If you keep the line number information, uncomment this to
+# hide the original source file name.
+#-renamesourcefileattribute SourceFile
+-keepattributes Signature
+
+# For using GSON @Expose annotation
+-keepattributes *Annotation*
+
+# Gson specific classes
+-keep class sun.misc.Unsafe { *; }
+#-keep class com.google.gson.stream.** { *; }
+
+# Application classes that will be serialized/deserialized over Gson
+-keep class com.quickblox.core.account.model.** { *; }
+
+-keep class com.quickblox.auth.parsers.** { *; }
+-keep class com.quickblox.auth.model.** { *; }
+-keep class com.quickblox.core.parser.** { *; }
+-keep class com.quickblox.core.model.** { *; }
+-keep class com.quickblox.core.server.** { *; }
+-keep class com.quickblox.core.rest.** { *; }
+-keep class com.quickblox.core.error.** { *; }
+-keep class com.quickblox.core.Query { *; }
+
+-keep class com.quickblox.content.model.** { *; }
+
+-keep class com.quickblox.users.parsers.** { *; }
+-keep class com.quickblox.users.model.** { *; }
+
+-keep class com.quickblox.messages.parsers.** { *; }
+-keep class com.quickblox.messages.QBPushNotifications { *; }
+-keep class com.quickblox.messages.model.** { *; }
+-keep class com.quickblox.messages.services.** { *; }
+
+-keep class com.quickblox.chat.parser.** { *; }
+-keep class com.quickblox.chat.model.** { *; }
+
+-keep class org.jivesoftware.** { *; }
+-keep class org.jxmpp.** { *; }
+-keep class org.webrtc.** { *; }
+-keep class com.quickblox.conference.** { *; }
+
+-keep class com.bumptech.** { *; }
+
+-dontwarn org.jivesoftware.smackx.**
\ No newline at end of file
diff --git a/videocall_webrtc_sample/android/app/src/debug/AndroidManifest.xml b/videocall_webrtc_sample/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..97b4804
--- /dev/null
+++ b/videocall_webrtc_sample/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/videocall_webrtc_sample/android/app/src/main/AndroidManifest.xml b/videocall_webrtc_sample/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..a79cae0
--- /dev/null
+++ b/videocall_webrtc_sample/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/videocall_webrtc_sample/android/app/src/main/kotlin/com/example/videocall_webrtc_sample/MainActivity.kt b/videocall_webrtc_sample/android/app/src/main/kotlin/com/example/videocall_webrtc_sample/MainActivity.kt
new file mode 100644
index 0000000..f5d80b8
--- /dev/null
+++ b/videocall_webrtc_sample/android/app/src/main/kotlin/com/example/videocall_webrtc_sample/MainActivity.kt
@@ -0,0 +1,6 @@
+package com.quickblox.videocall_webrtc_sample
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity: FlutterActivity() {
+}
diff --git a/videocall_webrtc_sample/android/app/src/main/res/drawable-v21/launch_background.xml b/videocall_webrtc_sample/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/videocall_webrtc_sample/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/videocall_webrtc_sample/android/app/src/main/res/drawable/launch_background.xml b/videocall_webrtc_sample/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/videocall_webrtc_sample/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/videocall_webrtc_sample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/videocall_webrtc_sample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..f0f7719
Binary files /dev/null and b/videocall_webrtc_sample/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/videocall_webrtc_sample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/videocall_webrtc_sample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..3c84f77
Binary files /dev/null and b/videocall_webrtc_sample/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/videocall_webrtc_sample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/videocall_webrtc_sample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..c945c8b
Binary files /dev/null and b/videocall_webrtc_sample/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/videocall_webrtc_sample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/videocall_webrtc_sample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..fa3eb7d
Binary files /dev/null and b/videocall_webrtc_sample/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/videocall_webrtc_sample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/videocall_webrtc_sample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..67eb7b2
Binary files /dev/null and b/videocall_webrtc_sample/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/videocall_webrtc_sample/android/app/src/main/res/values-night/styles.xml b/videocall_webrtc_sample/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/videocall_webrtc_sample/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/videocall_webrtc_sample/android/app/src/main/res/values/styles.xml b/videocall_webrtc_sample/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/videocall_webrtc_sample/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/videocall_webrtc_sample/android/app/src/profile/AndroidManifest.xml b/videocall_webrtc_sample/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..97b4804
--- /dev/null
+++ b/videocall_webrtc_sample/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,8 @@
+
+
+
+
diff --git a/videocall_webrtc_sample/android/build.gradle b/videocall_webrtc_sample/android/build.gradle
new file mode 100644
index 0000000..bc157bd
--- /dev/null
+++ b/videocall_webrtc_sample/android/build.gradle
@@ -0,0 +1,18 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+tasks.register("clean", Delete) {
+ delete rootProject.buildDir
+}
diff --git a/videocall_webrtc_sample/android/gradle.properties b/videocall_webrtc_sample/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/videocall_webrtc_sample/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/videocall_webrtc_sample/android/gradle/wrapper/gradle-wrapper.properties b/videocall_webrtc_sample/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3c472b9
--- /dev/null
+++ b/videocall_webrtc_sample/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip
diff --git a/videocall_webrtc_sample/android/settings.gradle b/videocall_webrtc_sample/android/settings.gradle
new file mode 100644
index 0000000..392eb2b
--- /dev/null
+++ b/videocall_webrtc_sample/android/settings.gradle
@@ -0,0 +1,26 @@
+pluginManagement {
+ def flutterSdkPath = {
+ def properties = new Properties()
+ file("local.properties").withInputStream { properties.load(it) }
+ def flutterSdkPath = properties.getProperty("flutter.sdk")
+ assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+ return flutterSdkPath
+ }
+ settings.ext.flutterSdkPath = flutterSdkPath()
+
+ includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id "dev.flutter.flutter-plugin-loader" version "1.0.0"
+ id "com.android.application" version "7.4.2" apply false
+ id "org.jetbrains.kotlin.android" version "1.7.10" apply false
+}
+
+include ":app"
\ No newline at end of file
diff --git a/videocall_webrtc_sample/assets/audio/beep.wav b/videocall_webrtc_sample/assets/audio/beep.wav
new file mode 100755
index 0000000..6cbc44a
Binary files /dev/null and b/videocall_webrtc_sample/assets/audio/beep.wav differ
diff --git a/videocall_webrtc_sample/assets/audio/ringtone.mp3 b/videocall_webrtc_sample/assets/audio/ringtone.mp3
new file mode 100644
index 0000000..2e6e603
Binary files /dev/null and b/videocall_webrtc_sample/assets/audio/ringtone.mp3 differ
diff --git a/videocall_webrtc_sample/assets/icons/accept.svg b/videocall_webrtc_sample/assets/icons/accept.svg
new file mode 100644
index 0000000..0c2c52c
--- /dev/null
+++ b/videocall_webrtc_sample/assets/icons/accept.svg
@@ -0,0 +1,3 @@
+
diff --git a/videocall_webrtc_sample/assets/icons/enable_camera.svg b/videocall_webrtc_sample/assets/icons/enable_camera.svg
new file mode 100644
index 0000000..924770f
--- /dev/null
+++ b/videocall_webrtc_sample/assets/icons/enable_camera.svg
@@ -0,0 +1,3 @@
+
diff --git a/videocall_webrtc_sample/assets/icons/exit.svg b/videocall_webrtc_sample/assets/icons/exit.svg
new file mode 100644
index 0000000..e1a5e92
--- /dev/null
+++ b/videocall_webrtc_sample/assets/icons/exit.svg
@@ -0,0 +1,16 @@
+
+А
\ No newline at end of file
diff --git a/videocall_webrtc_sample/assets/icons/hang_up.svg b/videocall_webrtc_sample/assets/icons/hang_up.svg
new file mode 100644
index 0000000..6ec5755
--- /dev/null
+++ b/videocall_webrtc_sample/assets/icons/hang_up.svg
@@ -0,0 +1,3 @@
+
diff --git a/videocall_webrtc_sample/assets/icons/launcher-logo.png b/videocall_webrtc_sample/assets/icons/launcher-logo.png
new file mode 100644
index 0000000..9dad2b4
Binary files /dev/null and b/videocall_webrtc_sample/assets/icons/launcher-logo.png differ
diff --git a/videocall_webrtc_sample/assets/icons/mute.svg b/videocall_webrtc_sample/assets/icons/mute.svg
new file mode 100644
index 0000000..e15640c
--- /dev/null
+++ b/videocall_webrtc_sample/assets/icons/mute.svg
@@ -0,0 +1,3 @@
+
diff --git a/videocall_webrtc_sample/assets/icons/qb-logo.svg b/videocall_webrtc_sample/assets/icons/qb-logo.svg
new file mode 100644
index 0000000..725def9
--- /dev/null
+++ b/videocall_webrtc_sample/assets/icons/qb-logo.svg
@@ -0,0 +1,11 @@
+
+
\ No newline at end of file
diff --git a/videocall_webrtc_sample/assets/icons/search.svg b/videocall_webrtc_sample/assets/icons/search.svg
new file mode 100644
index 0000000..278ff44
--- /dev/null
+++ b/videocall_webrtc_sample/assets/icons/search.svg
@@ -0,0 +1,15 @@
+
+
\ No newline at end of file
diff --git a/videocall_webrtc_sample/assets/icons/speaker.svg b/videocall_webrtc_sample/assets/icons/speaker.svg
new file mode 100644
index 0000000..5a38a24
--- /dev/null
+++ b/videocall_webrtc_sample/assets/icons/speaker.svg
@@ -0,0 +1,3 @@
+
diff --git a/videocall_webrtc_sample/assets/icons/swap_camera.svg b/videocall_webrtc_sample/assets/icons/swap_camera.svg
new file mode 100644
index 0000000..96c18d0
--- /dev/null
+++ b/videocall_webrtc_sample/assets/icons/swap_camera.svg
@@ -0,0 +1,3 @@
+
diff --git a/videocall_webrtc_sample/assets/icons/video.svg b/videocall_webrtc_sample/assets/icons/video.svg
new file mode 100644
index 0000000..438aafa
--- /dev/null
+++ b/videocall_webrtc_sample/assets/icons/video.svg
@@ -0,0 +1,3 @@
+
diff --git a/videocall_webrtc_sample/devtools_options.yaml b/videocall_webrtc_sample/devtools_options.yaml
new file mode 100644
index 0000000..7e7e7f6
--- /dev/null
+++ b/videocall_webrtc_sample/devtools_options.yaml
@@ -0,0 +1 @@
+extensions:
diff --git a/videocall_webrtc_sample/ios/.gitignore b/videocall_webrtc_sample/ios/.gitignore
new file mode 100644
index 0000000..7a7f987
--- /dev/null
+++ b/videocall_webrtc_sample/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/videocall_webrtc_sample/ios/Flutter/AppFrameworkInfo.plist b/videocall_webrtc_sample/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..7c56964
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,26 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+ MinimumOSVersion
+ 12.0
+
+
diff --git a/videocall_webrtc_sample/ios/Flutter/Debug.xcconfig b/videocall_webrtc_sample/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..ec97fc6
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/videocall_webrtc_sample/ios/Flutter/Release.xcconfig b/videocall_webrtc_sample/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..c4855bf
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/videocall_webrtc_sample/ios/Podfile b/videocall_webrtc_sample/ios/Podfile
new file mode 100644
index 0000000..279576f
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Podfile
@@ -0,0 +1,41 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '12.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+ use_modular_headers!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/videocall_webrtc_sample/ios/Podfile.lock b/videocall_webrtc_sample/ios/Podfile.lock
new file mode 100644
index 0000000..f6f3239
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Podfile.lock
@@ -0,0 +1,53 @@
+PODS:
+ - audioplayers_darwin (0.0.1):
+ - Flutter
+ - Flutter (1.0.0)
+ - path_provider_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
+ - permission_handler_apple (9.1.1):
+ - Flutter
+ - QuickBlox (2.19.0)
+ - Quickblox-WebRTC (2.8.1):
+ - QuickBlox (>= 2.7)
+ - quickblox_sdk (0.14.2):
+ - Flutter
+ - QuickBlox (~> 2.18)
+ - Quickblox-WebRTC (~> 2.7)
+
+DEPENDENCIES:
+ - audioplayers_darwin (from `.symlinks/plugins/audioplayers_darwin/ios`)
+ - Flutter (from `Flutter`)
+ - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
+ - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
+ - quickblox_sdk (from `.symlinks/plugins/quickblox_sdk/ios`)
+
+SPEC REPOS:
+ trunk:
+ - QuickBlox
+ - Quickblox-WebRTC
+
+EXTERNAL SOURCES:
+ audioplayers_darwin:
+ :path: ".symlinks/plugins/audioplayers_darwin/ios"
+ Flutter:
+ :path: Flutter
+ path_provider_foundation:
+ :path: ".symlinks/plugins/path_provider_foundation/darwin"
+ permission_handler_apple:
+ :path: ".symlinks/plugins/permission_handler_apple/ios"
+ quickblox_sdk:
+ :path: ".symlinks/plugins/quickblox_sdk/ios"
+
+SPEC CHECKSUMS:
+ audioplayers_darwin: 877d9a4d06331c5c374595e46e16453ac7eafa40
+ Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7
+ path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c
+ permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
+ QuickBlox: 3dba480339b42e77f9450773aaf4c3885463da3f
+ Quickblox-WebRTC: 10bb490c8f5ffe730a00b62549f059b386e99d8c
+ quickblox_sdk: 823736a1efcc32164069c21502ced09e48e1ff32
+
+PODFILE CHECKSUM: c4c93c5f6502fe2754f48404d3594bf779584011
+
+COCOAPODS: 1.15.2
diff --git a/videocall_webrtc_sample/ios/Runner.xcodeproj/project.pbxproj b/videocall_webrtc_sample/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..d3acd6c
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,568 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 6B5037A8B1E03CDEA3E8FF52 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9A044882BB3D1F207B8046EC /* Pods_Runner.framework */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 4802B1012BA47EBB00BE647E /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; };
+ 539AF109E7D5A0DE417C2118 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ 9A044882BB3D1F207B8046EC /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ 9C6BC595BFDC7F4D28F2709B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ D8FCDCA17B7E1F59DB05F908 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 6B5037A8B1E03CDEA3E8FF52 /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ C954E8208D3138DFA76CB8F1 /* Pods */,
+ FFB4FE0CC5578081A5074254 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 4802B1012BA47EBB00BE647E /* Runner.entitlements */,
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+ C954E8208D3138DFA76CB8F1 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 9C6BC595BFDC7F4D28F2709B /* Pods-Runner.debug.xcconfig */,
+ D8FCDCA17B7E1F59DB05F908 /* Pods-Runner.release.xcconfig */,
+ 539AF109E7D5A0DE417C2118 /* Pods-Runner.profile.xcconfig */,
+ );
+ path = Pods;
+ sourceTree = "";
+ };
+ FFB4FE0CC5578081A5074254 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ 9A044882BB3D1F207B8046EC /* Pods_Runner.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ DF7C7A38EB9A4E5BFB76B99A /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ 39F42ECF7A8431F8B0291A6C /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 39F42ECF7A8431F8B0291A6C /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+ DF7C7A38EB9A4E5BFB76B99A /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ 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 = 8885H5G2YX;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.quickblox.videocallWebrtcSample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 12.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ 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 = 8885H5G2YX;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.quickblox.videocallWebrtcSample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ 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 = 8885H5G2YX;
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.quickblox.videocallWebrtcSample;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ PROVISIONING_PROFILE_SPECIFIER = "";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/videocall_webrtc_sample/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/videocall_webrtc_sample/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/videocall_webrtc_sample/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/videocall_webrtc_sample/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/videocall_webrtc_sample/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/videocall_webrtc_sample/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/videocall_webrtc_sample/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/videocall_webrtc_sample/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..5e31d3d
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/videocall_webrtc_sample/ios/Runner.xcworkspace/contents.xcworkspacedata b/videocall_webrtc_sample/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/videocall_webrtc_sample/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/videocall_webrtc_sample/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/videocall_webrtc_sample/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/videocall_webrtc_sample/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/videocall_webrtc_sample/ios/Runner/AppDelegate.swift b/videocall_webrtc_sample/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..70693e4
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner/AppDelegate.swift
@@ -0,0 +1,13 @@
+import UIKit
+import Flutter
+
+@UIApplicationMain
+@objc class AppDelegate: FlutterAppDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ GeneratedPluginRegistrant.register(with: self)
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+}
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d36b1fa
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1,122 @@
+{
+ "images" : [
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-20x20@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-29x29@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-40x40@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "60x60",
+ "idiom" : "iphone",
+ "filename" : "Icon-App-60x60@3x.png",
+ "scale" : "3x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "20x20",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-20x20@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "29x29",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-29x29@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "40x40",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-40x40@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@1x.png",
+ "scale" : "1x"
+ },
+ {
+ "size" : "76x76",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-76x76@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "83.5x83.5",
+ "idiom" : "ipad",
+ "filename" : "Icon-App-83.5x83.5@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "size" : "1024x1024",
+ "idiom" : "ios-marketing",
+ "filename" : "Icon-App-1024x1024@1x.png",
+ "scale" : "1x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
new file mode 100644
index 0000000..c0ed1ed
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
new file mode 100644
index 0000000..c4b0d96
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
new file mode 100644
index 0000000..cac2155
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
new file mode 100644
index 0000000..5ce0e65
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
new file mode 100644
index 0000000..5bc5055
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
new file mode 100644
index 0000000..05caeaf
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
new file mode 100644
index 0000000..e90d0cc
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
new file mode 100644
index 0000000..cac2155
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
new file mode 100644
index 0000000..47e1ba5
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
new file mode 100644
index 0000000..40428b3
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
new file mode 100644
index 0000000..aefdd12
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
new file mode 100644
index 0000000..0d691d0
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
new file mode 100644
index 0000000..f6f7634
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
new file mode 100644
index 0000000..287a45a
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
new file mode 100644
index 0000000..40428b3
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
new file mode 100644
index 0000000..bc1402d
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
new file mode 100644
index 0000000..f0f7719
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
new file mode 100644
index 0000000..fa3eb7d
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
new file mode 100644
index 0000000..930e07e
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
new file mode 100644
index 0000000..72a45de
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
new file mode 100644
index 0000000..80317de
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/videocall_webrtc_sample/ios/Runner/Base.lproj/LaunchScreen.storyboard b/videocall_webrtc_sample/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/videocall_webrtc_sample/ios/Runner/Base.lproj/Main.storyboard b/videocall_webrtc_sample/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/videocall_webrtc_sample/ios/Runner/Info.plist b/videocall_webrtc_sample/ios/Runner/Info.plist
new file mode 100644
index 0000000..1239756
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner/Info.plist
@@ -0,0 +1,67 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ Videocall Webrtc Sample
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ videocall_webrtc_sample
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ NSBonjourServices
+
+ _dartobservatory._tcp
+
+ NSCameraUsageDescription
+ $(PRODUCT_NAME) would like to use your camera
+ NSLocationWhenInUseUsageDescription
+
+ NSMicrophoneUsageDescription
+ $(PRODUCT_NAME) would like to use your microphone to provide ability to record audio messages
+ UIApplicationSupportsIndirectInputEvents
+
+ UIBackgroundModes
+
+ processing
+ remote-notification
+ voip
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UIViewControllerBasedStatusBarAppearance
+
+
+
diff --git a/videocall_webrtc_sample/ios/Runner/Runner-Bridging-Header.h b/videocall_webrtc_sample/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/videocall_webrtc_sample/ios/Runner/Runner.entitlements b/videocall_webrtc_sample/ios/Runner/Runner.entitlements
new file mode 100644
index 0000000..0c67376
--- /dev/null
+++ b/videocall_webrtc_sample/ios/Runner/Runner.entitlements
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/videocall_webrtc_sample/lib/dependency/dependency.dart b/videocall_webrtc_sample/lib/dependency/dependency.dart
new file mode 100644
index 0000000..a8754c8
--- /dev/null
+++ b/videocall_webrtc_sample/lib/dependency/dependency.dart
@@ -0,0 +1,44 @@
+import '../managers/auth_manager.dart';
+import '../managers/chat_manager.dart';
+import '../managers/permission_manager.dart';
+import '../managers/ringtone_manager.dart';
+import '../managers/settings_manager.dart';
+import '../managers/storage_manager.dart';
+import '../managers/users_manager.dart';
+import '../managers/call_manager.dart';
+
+abstract class Dependency {
+ void init();
+
+ AuthManager getAuthManager();
+
+ ChatManager getChatManager();
+
+ SettingsManager getSettingsManager();
+
+ StorageManager getStorageManager();
+
+ UsersManager getUsersManager();
+
+ CallManager getCallManager();
+
+ PermissionManager getPermissionManager();
+
+ RingtoneManager getRingtoneManager();
+
+ void setAuthManager(AuthManager authManager);
+
+ void setChatManager(ChatManager chatManager);
+
+ void setSettingsManager(SettingsManager settingsManager);
+
+ void setStorageManager(StorageManager storageManager);
+
+ void setUsersManager(UsersManager usersManager);
+
+ void setWebRTCManager(CallManager webRTCManager);
+
+ void setPermissionManager(PermissionManager permissionManager);
+
+ void setRingtoneManager(RingtoneManager ringtoneManager);
+}
diff --git a/videocall_webrtc_sample/lib/dependency/dependency_exception.dart b/videocall_webrtc_sample/lib/dependency/dependency_exception.dart
new file mode 100644
index 0000000..3d5db39
--- /dev/null
+++ b/videocall_webrtc_sample/lib/dependency/dependency_exception.dart
@@ -0,0 +1,5 @@
+class DependencyException implements Exception {
+ String message;
+
+ DependencyException(this.message);
+}
diff --git a/videocall_webrtc_sample/lib/dependency/dependency_impl.dart b/videocall_webrtc_sample/lib/dependency/dependency_impl.dart
new file mode 100644
index 0000000..b49061d
--- /dev/null
+++ b/videocall_webrtc_sample/lib/dependency/dependency_impl.dart
@@ -0,0 +1,152 @@
+import 'package:videocall_webrtc_sample/dependency/dependency_exception.dart';
+import 'package:videocall_webrtc_sample/managers/auth_manager.dart';
+import 'package:videocall_webrtc_sample/managers/call_manager.dart';
+import 'package:videocall_webrtc_sample/managers/chat_manager.dart';
+import 'package:videocall_webrtc_sample/managers/ringtone_manager.dart';
+import 'package:videocall_webrtc_sample/managers/settings_manager.dart';
+import 'package:videocall_webrtc_sample/managers/storage_manager.dart';
+import 'package:videocall_webrtc_sample/managers/users_manager.dart';
+
+import '../managers/permission_manager.dart';
+import 'dependency.dart';
+
+class DependencyImpl implements Dependency {
+ DependencyImpl._();
+
+ static DependencyImpl? _instance;
+
+ static DependencyImpl getInstance() {
+ return _instance ??= DependencyImpl._();
+ }
+
+ AuthManager? _authManager;
+ ChatManager? _chatManager;
+ SettingsManager? _settingsManager;
+ StorageManager? _storageManager;
+ UsersManager? _usersManager;
+ CallManager? _callManager;
+ PermissionManager? _permissionManager;
+ RingtoneManager? _ringtoneManager;
+
+ @override
+ Future init() async {
+ _authManager = AuthManager();
+ _chatManager = ChatManager();
+ _settingsManager = SettingsManager();
+ _storageManager = StorageManager();
+ await _storageManager?.init();
+
+ _usersManager = UsersManager();
+ _callManager = CallManager();
+ _permissionManager = PermissionManager();
+ _ringtoneManager = RingtoneManager();
+ }
+
+ void _throwDIException(String text) {
+ throw DependencyException("The $text is not initialized. First, you need to init dependency.");
+ }
+
+ @override
+ AuthManager getAuthManager() {
+ if (_authManager == null) {
+ _throwDIException("AuthManager");
+ }
+ return _authManager!;
+ }
+
+ @override
+ ChatManager getChatManager() {
+ if (_chatManager == null) {
+ _throwDIException("ChatManager");
+ }
+ return _chatManager!;
+ }
+
+ @override
+ SettingsManager getSettingsManager() {
+ if (_settingsManager == null) {
+ _throwDIException("SettingsManager");
+ }
+ return _settingsManager!;
+ }
+
+ @override
+ StorageManager getStorageManager() {
+ if (_storageManager == null) {
+ _throwDIException("StorageManager");
+ }
+ return _storageManager!;
+ }
+
+ @override
+ UsersManager getUsersManager() {
+ if (_usersManager == null) {
+ _throwDIException("UsersManager");
+ }
+ return _usersManager!;
+ }
+
+ @override
+ CallManager getCallManager() {
+ if (_callManager == null) {
+ _throwDIException("CallManager");
+ }
+ return _callManager!;
+ }
+
+ @override
+ PermissionManager getPermissionManager() {
+ if (_permissionManager == null) {
+ _throwDIException("PermissionManager");
+ }
+ return _permissionManager!;
+ }
+
+ @override
+ RingtoneManager getRingtoneManager() {
+ if (_ringtoneManager == null) {
+ _throwDIException("RingtoneManager");
+ }
+ return _ringtoneManager!;
+ }
+
+ @override
+ void setAuthManager(AuthManager authManager) {
+ _authManager = authManager;
+ }
+
+ @override
+ void setChatManager(ChatManager chatManager) {
+ _chatManager = chatManager;
+ }
+
+ @override
+ void setPermissionManager(PermissionManager permissionManager) {
+ _permissionManager = permissionManager;
+ }
+
+ @override
+ void setRingtoneManager(RingtoneManager ringtoneManager) {
+ _ringtoneManager = ringtoneManager;
+ }
+
+ @override
+ void setSettingsManager(SettingsManager settingsManager) {
+ _settingsManager = settingsManager;
+ }
+
+ @override
+ void setStorageManager(StorageManager storageManager) {
+ _storageManager = storageManager;
+ }
+
+ @override
+ void setUsersManager(UsersManager usersManager) {
+ _usersManager = usersManager;
+ }
+
+ @override
+ void setWebRTCManager(CallManager callManager) {
+ _callManager = callManager;
+ }
+}
diff --git a/videocall_webrtc_sample/lib/entities/user_entity.dart b/videocall_webrtc_sample/lib/entities/user_entity.dart
new file mode 100644
index 0000000..2381f21
--- /dev/null
+++ b/videocall_webrtc_sample/lib/entities/user_entity.dart
@@ -0,0 +1,24 @@
+import 'package:quickblox_sdk/models/qb_user.dart';
+
+class UserEntity {
+ bool selected;
+ final QBUser? user;
+
+ int? get userId => user?.id;
+
+ String? get name => user?.fullName ?? user?.login;
+
+ UserEntity(this.selected, this.user);
+
+ @override
+ bool operator ==(Object other) {
+ return other is UserEntity && other.runtimeType == runtimeType && other.userId == userId;
+ }
+
+ @override
+ int get hashCode {
+ int hash = 3;
+ hash = 53 * hash + userId.toString().length;
+ return hash;
+ }
+}
diff --git a/videocall_webrtc_sample/lib/entities/video_call_entity.dart b/videocall_webrtc_sample/lib/entities/video_call_entity.dart
new file mode 100644
index 0000000..b6d9798
--- /dev/null
+++ b/videocall_webrtc_sample/lib/entities/video_call_entity.dart
@@ -0,0 +1,39 @@
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:quickblox_sdk/webrtc/rtc_video_view.dart';
+
+class VideoCallEntity {
+ RTCVideoViewController? _controller;
+
+ set controller(RTCVideoViewController value) {
+ _controller = value;
+ }
+
+ final QBUser? _user;
+
+ int? get userId => _user?.id;
+
+ String? get name => _user?.fullName ?? _user?.login;
+
+ VideoCallEntity(this._user);
+
+ Future playVideo(String sessionId) async {
+ await _controller?.play(sessionId, userId!);
+ }
+
+ Future releaseVideo() async {
+ await _controller?.release();
+ }
+
+ @override
+ bool operator ==(Object other) {
+ if (identical(this, other)) return true;
+ return other is VideoCallEntity && other.userId == userId;
+ }
+
+ @override
+ int get hashCode {
+ int hash = 7;
+ hash = 31 * hash + (userId != null ? userId.hashCode : 0);
+ return hash;
+ }
+}
diff --git a/videocall_webrtc_sample/lib/main.dart b/videocall_webrtc_sample/lib/main.dart
new file mode 100644
index 0000000..244cc94
--- /dev/null
+++ b/videocall_webrtc_sample/lib/main.dart
@@ -0,0 +1,54 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:videocall_webrtc_sample/managers/lifecycle_manage.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/splash/splash_screen.dart';
+
+import 'dependency/dependency_impl.dart';
+
+const String APPLICATION_ID = "";
+const String AUTH_KEY = "";
+const String AUTH_SECRET = "";
+const String ACCOUNT_KEY = "";
+
+const String ICE_SEVER_URL = "";
+const String ICE_SERVER_USER = "";
+const String ICE_SERVER_PASSWORD = "";
+
+Future main() async {
+ await DependencyImpl.getInstance().init();
+ runApp(const MyApp());
+}
+
+class MyApp extends StatefulWidget {
+ const MyApp({super.key});
+
+ @override
+ State createState() => _MyAppState();
+}
+
+class _MyAppState extends State {
+ final LifecycleManager _lifecycleManager = LifecycleManager();
+
+ @override
+ void initState() {
+ WidgetsBinding.instance.addObserver(_lifecycleManager);
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ WidgetsBinding.instance.removeObserver(_lifecycleManager);
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
+
+ return MaterialApp(
+ title: 'Flutter Demo',
+ theme: ThemeData(primarySwatch: Colors.blue),
+ home: const SplashScreen(),
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/managers/auth_manager.dart b/videocall_webrtc_sample/lib/managers/auth_manager.dart
new file mode 100644
index 0000000..1aaf0ea
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/auth_manager.dart
@@ -0,0 +1,21 @@
+import 'package:quickblox_sdk/auth/module.dart';
+import 'package:quickblox_sdk/models/qb_session.dart';
+import 'package:quickblox_sdk/quickblox_sdk.dart';
+
+class AuthManager {
+ Future login(String login, String password) async {
+ return await QB.auth.login(login, password);
+ }
+
+ Future logout() async {
+ await QB.auth.logout();
+ }
+
+ Future createSession(QBSession qbSession) async {
+ return QB.auth.setSession(qbSession) as QBSession;
+ }
+
+ Future getSession() async {
+ return QB.auth.getSession() as QBSession;
+ }
+}
diff --git a/videocall_webrtc_sample/lib/managers/call_manager.dart b/videocall_webrtc_sample/lib/managers/call_manager.dart
new file mode 100644
index 0000000..30211a5
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/call_manager.dart
@@ -0,0 +1,381 @@
+import 'dart:async';
+import 'dart:collection';
+
+import 'package:quickblox_sdk/models/qb_rtc_session.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:quickblox_sdk/quickblox_sdk.dart';
+import 'package:quickblox_sdk/webrtc/constants.dart';
+import 'package:videocall_webrtc_sample/entities/video_call_entity.dart';
+
+import '../mappers/qb_rtc_session_mapper.dart';
+import 'callback/call_subscription.dart';
+
+enum CallTypes { AUDIO, VIDEO }
+
+enum AudioOutputTypes { EAR_SPEAKER, LOUDSPEAKER, HEADPHONES, BLUETOOTH }
+
+class CallManager {
+ final Set _callSubscriptions = {};
+
+ final LinkedHashSet _videoCallEntities = LinkedHashSet();
+
+ LinkedHashSet get videoCallEntities => _videoCallEntities;
+
+ QBRTCSession? _session;
+
+ StreamSubscription? _incomeCallSubscription;
+ StreamSubscription? _callEndSubscription;
+ StreamSubscription? _rejectSubscription;
+ StreamSubscription? _acceptSubscription;
+ StreamSubscription? _hangUpSubscription;
+ StreamSubscription? _notAnswerSubscription;
+ StreamSubscription? _videoTracksSubscription;
+
+ Future subscribeCall(CallSubscription? callSubscription) async {
+ if (callSubscription != null) {
+ _callSubscriptions.add(callSubscription);
+ }
+ }
+
+ Future unsubscribeCall(CallSubscription? callSubscription) async {
+ _callSubscriptions.remove(callSubscription);
+ }
+
+ void _notifyIncome(QBRTCSession? session) {
+ for (var callSubscription in _callSubscriptions) {
+ callSubscription.onIncomingCall(session);
+ }
+ }
+
+ void _notifyReject(int opponentId) {
+ for (var callSubscription in _callSubscriptions) {
+ callSubscription.onRejectCall(opponentId);
+ }
+ }
+
+ void _notifyAccept(int opponentId) {
+ for (var callSubscription in _callSubscriptions) {
+ callSubscription.onAcceptCall(opponentId);
+ }
+ }
+
+ void _notifyHangup(int opponentId) {
+ for (var callSubscription in _callSubscriptions) {
+ callSubscription.onHangupCall(opponentId);
+ }
+ }
+
+ void _notifyNotAnswer(int opponentId) {
+ for (var callSubscription in _callSubscriptions) {
+ callSubscription.onNotAnswer(opponentId);
+ }
+ }
+
+ void _notifyReceivedVideoTrack(int opponentId) {
+ for (var callSubscription in _callSubscriptions) {
+ callSubscription.onReceivedVideoTrack(opponentId);
+ }
+ }
+
+ void _notifyCallEnd() {
+ for (var callSubscription in _callSubscriptions) {
+ callSubscription.onEndCall();
+ }
+ }
+
+ void _notifyError(String error) {
+ for (var callSubscription in _callSubscriptions) {
+ callSubscription.onError(error);
+ }
+ }
+
+ void addVideoCallEntities(Iterable users) {
+ for (final user in users) {
+ VideoCallEntity entity = VideoCallEntity(user);
+ videoCallEntities.add(entity);
+ }
+ }
+
+ void _clearVideoCallEntities() {
+ videoCallEntities.clear();
+ }
+
+ bool isActiveCall() {
+ return _session != null;
+ }
+
+ Future enableAudio(bool enable) async {
+ if (_session != null) {
+ String sessionId = _session?.id ?? "";
+ await QB.webrtc.enableAudio(sessionId, enable: enable);
+ }
+ }
+
+ Future enableVideo(bool enable) async {
+ if (_session != null) {
+ String sessionId = _session?.id ?? "";
+ await QB.webrtc.enableVideo(sessionId, enable: enable);
+ }
+ }
+
+ Future switchAudio(AudioOutputTypes audioOutputType) async {
+ int outputType = _parseAudioOutputType(audioOutputType);
+ await QB.webrtc.switchAudioOutput(outputType);
+ }
+
+ Future switchCamera() async {
+ if (_session != null) {
+ String sessionId = _session?.id ?? "";
+ await QB.webrtc.switchCamera(sessionId);
+ }
+ }
+
+ int _parseAudioOutputType(AudioOutputTypes outputType) {
+ switch (outputType) {
+ case AudioOutputTypes.EAR_SPEAKER:
+ return 0;
+ case AudioOutputTypes.LOUDSPEAKER:
+ return 1;
+ case AudioOutputTypes.HEADPHONES:
+ return 2;
+ case AudioOutputTypes.BLUETOOTH:
+ return 3;
+ }
+ }
+
+ Future startAudioCall(List users) async {
+ List userIds = _getUserIdsFrom(users);
+ await _createSession(userIds, CallTypes.AUDIO);
+ }
+
+ Future startVideoCall(List users) async {
+ List userIds = _getUserIdsFrom(users);
+ await _createSession(userIds, CallTypes.VIDEO);
+ }
+
+ List _getUserIdsFrom(List users) {
+ List list = [];
+ for (final user in users) {
+ if (user?.id != null) {
+ list.add(user!.id!);
+ }
+ }
+ return list;
+ }
+
+ Future _createSession(List opponentsIds, CallTypes callType) async {
+ int sessionCallType = _parseCallType(callType);
+ QBRTCSession? session = await QB.webrtc.call(opponentsIds, sessionCallType);
+ _session = session;
+ return session;
+ }
+
+ int _parseCallType(CallTypes callType) {
+ switch (callType) {
+ case CallTypes.VIDEO:
+ return 1;
+ case CallTypes.AUDIO:
+ return 2;
+ }
+ }
+
+ Future rejectCall() async {
+ if (_session != null) {
+ String sessionId = _session?.id ?? "";
+ await QB.webrtc.reject(sessionId);
+ }
+ }
+
+ Future acceptCall() async {
+ if (_session != null) {
+ String sessionId = _session?.id ?? "";
+ await QB.webrtc.accept(sessionId);
+ }
+ }
+
+ Future hangUpCall() async {
+ if (_session != null) {
+ String sessionId = _session?.id ?? "";
+ QB.webrtc.hangUp(sessionId);
+ }
+ }
+
+ Future?> getOpponentIdsFromCall() async {
+ List? opponentsIds = _session?.opponentsIds;
+ int? initiatorId = _session?.initiatorId;
+ if (initiatorId != null) {
+ opponentsIds?.add(initiatorId);
+ }
+ return opponentsIds;
+ }
+
+ Future initAndSubscribeEvents() async {
+ await _setRTCConfigs();
+ await QB.webrtc.init();
+ await _subscribeEvents();
+ }
+
+ Future _setRTCConfigs() async {
+ await QB.rtcConfig.setAnswerTimeInterval(30);
+ await QB.rtcConfig.setDialingTimeInterval(15);
+ }
+
+ Future _subscribeEvents() async {
+ await _subscribeIncoming();
+ await _subscribeHangUp();
+ await _subscribeCallEnd();
+ await _subscribeAccept();
+ await _subscribeReject();
+ await _subscribeNotAnswer();
+ await _subscribeVideoTracks();
+ }
+
+ Future _subscribeIncoming() async {
+ _incomeCallSubscription = await QB.webrtc.subscribeRTCEvent(QBRTCEventTypes.CALL, (data) async {
+ final session = _parseSessionFrom(data);
+ if (isActiveCall()) {
+ await QB.webrtc.reject(session!.id!);
+ } else {
+ _session = session;
+ _notifyIncome(_session);
+ }
+ }, onErrorMethod: (e) => _notifyError(e.toString()));
+ }
+
+ QBRTCSession? _parseSessionFrom(dynamic data) {
+ Map payloadMap = Map.from(data["payload"]);
+ Map sessionMap = Map.from(payloadMap["session"]);
+
+ final session = QBRTCSessionMapper.mapToQBRtcSession(sessionMap);
+ return session;
+ }
+
+ Future _subscribeCallEnd() async {
+ _callEndSubscription =
+ await QB.webrtc.subscribeRTCEvent(QBRTCEventTypes.CALL_END, (data) async {
+ final session = _parseSessionFrom(data);
+ if (session?.id != _session?.id) {
+ return;
+ }
+
+ _session = null;
+ await _releaseViews();
+ _clearVideoCallEntities();
+
+ _notifyCallEnd();
+ }, onErrorMethod: (e) => _notifyError(e.toString()));
+ }
+
+ Future _subscribeNotAnswer() async {
+ _notAnswerSubscription = await QB.webrtc.subscribeRTCEvent(QBRTCEventTypes.NOT_ANSWER, (data) {
+ int opponentId = data["payload"]["userId"];
+
+ _notifyNotAnswer(opponentId);
+ }, onErrorMethod: (e) => _notifyError(e.toString()));
+ }
+
+ Future _subscribeReject() async {
+ _rejectSubscription = await QB.webrtc.subscribeRTCEvent(QBRTCEventTypes.REJECT, (data) {
+ int opponentId = data["payload"]["userId"];
+
+ _notifyReject(opponentId);
+ }, onErrorMethod: (e) => _notifyError(e.toString()));
+ }
+
+ Future _subscribeAccept() async {
+ _acceptSubscription = await QB.webrtc.subscribeRTCEvent(QBRTCEventTypes.ACCEPT, (data) {
+ int opponentId = data["payload"]["userId"];
+ _notifyAccept(opponentId);
+ }, onErrorMethod: (e) => _notifyError(e.toString()));
+ }
+
+ Future _subscribeHangUp() async {
+ _hangUpSubscription = await QB.webrtc.subscribeRTCEvent(QBRTCEventTypes.HANG_UP, (data) {
+ int opponentId = data["payload"]["userId"];
+
+ VideoCallEntity? entity = getEntityBy(opponentId);
+ entity?.releaseVideo();
+
+ _notifyHangup(opponentId);
+ }, onErrorMethod: (e) => _notifyError(e.toString()));
+ }
+
+ VideoCallEntity? getEntityBy(int opponentId) {
+ try {
+ return _videoCallEntities.firstWhere((element) => element.userId == opponentId);
+ } on StateError catch (e) {
+ return null;
+ }
+ }
+
+ Future _subscribeVideoTracks() async {
+ _videoTracksSubscription =
+ await QB.webrtc.subscribeRTCEvent(QBRTCEventTypes.RECEIVED_VIDEO_TRACK, (data) {
+ Map payloadMap = Map.from(data["payload"]);
+
+ int opponentId = payloadMap["userId"];
+
+ VideoCallEntity? entity = getEntityBy(opponentId);
+ entity?.playVideo(_session!.id!);
+
+ _notifyReceivedVideoTrack(opponentId);
+ });
+ }
+
+ Future release() async {
+ await unsubscribeEvents();
+ await _releaseViews();
+ await QB.webrtc.release();
+ }
+
+ Future unsubscribeEvents() async {
+ _unsubscribeIncoming();
+ _unsubscribeCallEnd();
+ _unsubscribeReject();
+ _unsubscribeAccept();
+ _unsubscribeHangup();
+ _unsubscribeNotAnswer();
+ _unsubscribeVideoTracks();
+ }
+
+ Future _releaseViews() async {
+ for (var entity in _videoCallEntities) {
+ await entity.releaseVideo();
+ }
+ }
+
+ Future _unsubscribeIncoming() async {
+ await _incomeCallSubscription?.cancel();
+ _incomeCallSubscription = null;
+ }
+
+ Future _unsubscribeCallEnd() async {
+ await _callEndSubscription?.cancel();
+ _callEndSubscription = null;
+ }
+
+ void _unsubscribeReject() {
+ _rejectSubscription?.cancel();
+ _rejectSubscription = null;
+ }
+
+ void _unsubscribeAccept() {
+ _acceptSubscription?.cancel();
+ _acceptSubscription = null;
+ }
+
+ void _unsubscribeHangup() {
+ _hangUpSubscription?.cancel();
+ _hangUpSubscription = null;
+ }
+
+ void _unsubscribeNotAnswer() {
+ _notAnswerSubscription?.cancel();
+ _notAnswerSubscription = null;
+ }
+
+ void _unsubscribeVideoTracks() {
+ _videoTracksSubscription?.cancel();
+ _videoTracksSubscription = null;
+ }
+}
diff --git a/videocall_webrtc_sample/lib/managers/callback/call_subscription.dart b/videocall_webrtc_sample/lib/managers/callback/call_subscription.dart
new file mode 100644
index 0000000..b500e5e
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/callback/call_subscription.dart
@@ -0,0 +1,19 @@
+import 'package:quickblox_sdk/models/qb_rtc_session.dart';
+
+abstract class CallSubscription {
+ void onIncomingCall(QBRTCSession? session);
+
+ void onRejectCall(int opponentId);
+
+ void onAcceptCall(int opponentId);
+
+ void onHangupCall(int opponentId);
+
+ void onNotAnswer(int opponentId);
+
+ void onEndCall();
+
+ void onReceivedVideoTrack(int opponentId);
+
+ void onError(String error);
+}
diff --git a/videocall_webrtc_sample/lib/managers/callback/call_subscription_impl.dart b/videocall_webrtc_sample/lib/managers/callback/call_subscription_impl.dart
new file mode 100644
index 0000000..3af3884
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/callback/call_subscription_impl.dart
@@ -0,0 +1,103 @@
+import 'package:quickblox_sdk/models/qb_rtc_session.dart';
+
+import 'call_subscription.dart';
+
+class CallSubscriptionImpl implements CallSubscription {
+ CallSubscriptionImpl({
+ required String tag,
+ void Function(QBRTCSession? session)? onIncomingCall,
+ void Function(int opponentId)? onCallRejected,
+ void Function(int opponentId)? onCallAccept,
+ void Function(int opponentId)? onHangup,
+ void Function(int opponentId)? onNotAnswer,
+ void Function(int opponentId)? onReceivedVideoTrack,
+ void Function()? onCallEnd,
+ void Function(String error)? onError,
+ }) : _tag = tag,
+ _onIncomingCall = onIncomingCall,
+ _onCallRejected = onCallRejected,
+ _onCallAccept = onCallAccept,
+ _onHangup = onHangup,
+ _onNotAnswer = onNotAnswer,
+ _onReceivedVideoTrack = onReceivedVideoTrack,
+ _onCallEnd = onCallEnd,
+ _onError = onError;
+
+ final String _tag;
+ final void Function(QBRTCSession? session)? _onIncomingCall;
+ final void Function(int opponentId)? _onCallRejected;
+ final void Function(int opponentId)? _onCallAccept;
+ final void Function(int opponentId)? _onHangup;
+ final void Function(int opponentId)? _onNotAnswer;
+ final void Function(int opponentId)? _onReceivedVideoTrack;
+ final void Function()? _onCallEnd;
+ final void Function(String error)? _onError;
+
+ @override
+ void onIncomingCall(QBRTCSession? session) {
+ if (_onIncomingCall != null) {
+ _onIncomingCall!(session);
+ }
+ }
+
+ @override
+ void onRejectCall(int opponentId) {
+ if (_onCallRejected != null) {
+ _onCallRejected!(opponentId);
+ }
+ }
+
+ @override
+ void onAcceptCall(int opponentId) {
+ if (_onCallAccept != null) {
+ _onCallAccept!(opponentId);
+ }
+ }
+
+ @override
+ void onHangupCall(int opponentId) {
+ if (_onHangup != null) {
+ _onHangup!(opponentId);
+ }
+ }
+
+ @override
+ void onNotAnswer(int opponentId) {
+ if (_onNotAnswer != null) {
+ _onNotAnswer!(opponentId);
+ }
+ }
+
+ @override
+ void onEndCall() {
+ if (_onCallEnd != null) {
+ _onCallEnd!();
+ }
+ }
+
+ @override
+ void onError(String error) {
+ if (_onError != null) {
+ _onError!(error);
+ }
+ }
+
+ @override
+ void onReceivedVideoTrack(int opponentId) {
+ if (_onReceivedVideoTrack != null) {
+ _onReceivedVideoTrack!(opponentId);
+ }
+ }
+
+ @override
+ bool operator ==(Object other) {
+ return other is CallSubscriptionImpl && other._tag == _tag;
+ }
+
+ @override
+ int get hashCode {
+ int hash = 3;
+ hash = 53 * hash + _tag.length;
+ return hash;
+ }
+}
diff --git a/videocall_webrtc_sample/lib/managers/chat_manager.dart b/videocall_webrtc_sample/lib/managers/chat_manager.dart
new file mode 100644
index 0000000..c3d8b32
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/chat_manager.dart
@@ -0,0 +1,15 @@
+import 'package:quickblox_sdk/quickblox_sdk.dart';
+
+class ChatManager {
+ Future connect(int userId, String password) async {
+ await QB.chat.connect(userId, password);
+ }
+
+ Future disconnect() async {
+ await QB.chat.disconnect();
+ }
+
+ Future isConnected() async {
+ return QB.chat.isConnected();
+ }
+}
diff --git a/videocall_webrtc_sample/lib/managers/lifecycle_manage.dart b/videocall_webrtc_sample/lib/managers/lifecycle_manage.dart
new file mode 100644
index 0000000..e62b962
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/lifecycle_manage.dart
@@ -0,0 +1,73 @@
+import 'dart:developer';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:videocall_webrtc_sample/managers/storage_manager.dart';
+import 'package:videocall_webrtc_sample/managers/call_manager.dart';
+
+import '../dependency/dependency_impl.dart';
+import 'chat_manager.dart';
+
+class LifecycleManager with WidgetsBindingObserver {
+ final ChatManager _chatManager = DependencyImpl.getInstance().getChatManager();
+ final StorageManager _storageManager = DependencyImpl.getInstance().getStorageManager();
+ final CallManager _callManager = DependencyImpl.getInstance().getCallManager();
+
+ @override
+ void didChangeAppLifecycleState(AppLifecycleState state) async {
+ switch (state) {
+ case AppLifecycleState.resumed:
+ try {
+ bool isExistSavedUser = await _storageManager.isExistSavedUser();
+ bool isNotConnectedToChat = await _isNotConnectedToChat();
+ if (isExistSavedUser && isNotConnectedToChat) {
+ await _connectToChat();
+ await _callManager.initAndSubscribeEvents();
+ }
+ } on PlatformException catch (e) {
+ //TODO: Need to add logic of show exception to screen.
+ log('Error occurred: $e', error: e);
+ }
+ break;
+ case AppLifecycleState.detached:
+ try {
+ bool isHasActiveCall = _callManager.isActiveCall();
+ if (isHasActiveCall) {
+ _callManager.hangUpCall();
+ }
+ _callManager.release();
+ _chatManager.disconnect();
+ } on PlatformException catch (e) {
+ log('Error occurred: $e', error: e);
+ }
+ break;
+ case AppLifecycleState.hidden:
+ try {
+ bool isNotHasActiveCall = !_callManager.isActiveCall();
+ if (isNotHasActiveCall) {
+ _callManager.release();
+ _chatManager.disconnect();
+ }
+ } on PlatformException catch (e) {
+ log('Error occurred: $e', error: e);
+ }
+ break;
+ case AppLifecycleState.paused:
+ case AppLifecycleState.inactive:
+ break;
+ }
+ }
+
+ Future _isNotConnectedToChat() async {
+ bool isConnectedToChat = await _chatManager.isConnected() ?? false;
+ bool isNotConnectedToChat = !isConnectedToChat;
+ return isNotConnectedToChat;
+ }
+
+ Future _connectToChat() async {
+ final userId = await _storageManager.getLoggedUserId();
+ final userPassword = await _storageManager.getUserPassword();
+ await _chatManager.connect(userId, userPassword);
+ }
+}
diff --git a/videocall_webrtc_sample/lib/managers/permission_manager.dart b/videocall_webrtc_sample/lib/managers/permission_manager.dart
new file mode 100644
index 0000000..29082b0
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/permission_manager.dart
@@ -0,0 +1,59 @@
+import 'package:permission_handler/permission_handler.dart';
+
+class PermissionManager {
+ Future checkPermissionsForAudioCall() async {
+ bool microphoneDenied = await Permission.microphone.status.isDenied;
+ bool bluetoothDenied = await Permission.bluetoothConnect.status.isDenied;
+ bool isAllPermissionsGranted = true;
+
+ if (microphoneDenied || bluetoothDenied) {
+ Map statuses =
+ await [Permission.bluetoothConnect, Permission.microphone].request();
+ isAllPermissionsGranted = _isAllPermissionsGranted(statuses);
+ }
+
+ return isAllPermissionsGranted;
+ }
+
+ Future checkPermissionsForVideoCall() async {
+ bool cameraDenied = await Permission.camera.status.isDenied;
+ bool microphoneDenied = await Permission.microphone.status.isDenied;
+ bool bluetoothDenied = await Permission.bluetoothConnect.status.isDenied;
+
+ bool isAllPermissionsGranted = true;
+
+ if (cameraDenied || microphoneDenied || bluetoothDenied) {
+ Map statuses =
+ await [Permission.bluetoothConnect, Permission.camera, Permission.microphone].request();
+
+ isAllPermissionsGranted = _isAllPermissionsGranted(statuses);
+ }
+
+ return isAllPermissionsGranted;
+ }
+
+ Future checkNotificationPermission() async {
+ bool notificationDenied = await Permission.notification.status.isDenied;
+
+ bool isAllPermissionsGranted = true;
+
+ if (notificationDenied) {
+ Map statuses = await [Permission.notification].request();
+ isAllPermissionsGranted = _isAllPermissionsGranted(statuses);
+ }
+
+ return isAllPermissionsGranted;
+ }
+
+ bool _isAllPermissionsGranted(Map statuses) {
+ bool isAllPermissionsGranted = true;
+ statuses.forEach((key, value) {
+ if (value == PermissionStatus.denied || value == PermissionStatus.permanentlyDenied) {
+ isAllPermissionsGranted = false;
+ return;
+ }
+ });
+
+ return isAllPermissionsGranted;
+ }
+}
diff --git a/videocall_webrtc_sample/lib/managers/ringtone_manager.dart b/videocall_webrtc_sample/lib/managers/ringtone_manager.dart
new file mode 100644
index 0000000..b07f641
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/ringtone_manager.dart
@@ -0,0 +1,23 @@
+import 'package:audioplayers/audioplayers.dart';
+
+class RingtoneManager {
+ AudioPlayer? audioPlayer;
+
+ Future startRingtone() async {
+ audioPlayer ??= AudioPlayer();
+ await audioPlayer?.setReleaseMode(ReleaseMode.loop);
+ await audioPlayer?.setPlayerMode(PlayerMode.mediaPlayer);
+ await audioPlayer?.play(AssetSource('audio/ringtone.mp3'), volume: 1.0);
+ }
+
+ Future release() async {
+ await audioPlayer?.release();
+ audioPlayer = null;
+ }
+
+ Future startBeeps() async {
+ audioPlayer ??= AudioPlayer();
+ await audioPlayer?.setReleaseMode(ReleaseMode.loop);
+ await audioPlayer?.play(AssetSource('audio/beep.wav'), volume: 1.0);
+ }
+}
diff --git a/videocall_webrtc_sample/lib/managers/settings_manager.dart b/videocall_webrtc_sample/lib/managers/settings_manager.dart
new file mode 100644
index 0000000..0556192
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/settings_manager.dart
@@ -0,0 +1,42 @@
+import 'dart:async';
+
+import 'package:quickblox_sdk/models/qb_ice_server.dart';
+import 'package:quickblox_sdk/models/qb_settings.dart';
+import 'package:quickblox_sdk/quickblox_sdk.dart';
+
+class SettingsManager {
+ Future init(String appId, String authKey, String authSecret, String accountKey,
+ {String? apiEndpoint, String? chatEndpoint}) async {
+ await QB.settings.init(appId, authKey, authSecret, accountKey,
+ apiEndpoint: apiEndpoint, chatEndpoint: chatEndpoint);
+ }
+
+ Future get() async {
+ return await QB.settings.get();
+ }
+
+ Future initStreamManagement(bool autoReconnect, int messageTimeout) async {
+ await QB.settings.initStreamManagement(messageTimeout, autoReconnect: autoReconnect);
+ }
+
+ Future enableXMPPLogging() async {
+ await QB.settings.enableXMPPLogging();
+ }
+
+ Future enableLogging() async {
+ await QB.settings.enableLogging();
+ }
+
+ Future enableAutoReconnect(bool enable) async {
+ await QB.settings.enableAutoReconnect(enable);
+ }
+
+ Future setIceServers(String url, String userName, String password) async {
+ QBIceServer server = QBIceServer();
+ server.url = url;
+ server.userName = userName;
+ server.password = password;
+
+ await QB.rtcConfig.setIceServers([server]);
+ }
+}
diff --git a/videocall_webrtc_sample/lib/managers/storage_manager.dart b/videocall_webrtc_sample/lib/managers/storage_manager.dart
new file mode 100644
index 0000000..c66da57
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/storage_manager.dart
@@ -0,0 +1,60 @@
+import 'dart:async';
+
+import 'package:hive_flutter/hive_flutter.dart';
+
+class StorageManager {
+ static const int _NOT_SAVED_USER_ID = -1;
+ static const String _USER_ID_KEY = "user_id_key";
+ static const String _USER_LOGIN_KEY = "user_login_key";
+ static const String _USER_NAME_KEY = "user_name_key";
+ static const String _USER_PASSWORD_KEY = "user_password_key";
+
+ Box? _hiveBox;
+
+ Future init() async {
+ await Hive.initFlutter();
+ _hiveBox = await Hive.openBox("storage");
+ }
+
+ Future getLoggedUserId() async {
+ return await _hiveBox?.get(_USER_ID_KEY) ?? _NOT_SAVED_USER_ID;
+ }
+
+ Future getUserLogin() async {
+ return await _hiveBox?.get(_USER_LOGIN_KEY, defaultValue: "");
+ }
+
+ Future getNameLogin() async {
+ return await _hiveBox?.get(_USER_NAME_KEY, defaultValue: "");
+ }
+
+ Future getUserPassword() async {
+ return await _hiveBox?.get(_USER_PASSWORD_KEY, defaultValue: "");
+ }
+
+ Future saveUserId(int userId) async {
+ await _hiveBox?.put(_USER_ID_KEY, userId);
+ }
+
+ Future saveUserLogin(String login) async {
+ await _hiveBox?.put(_USER_LOGIN_KEY, login);
+ }
+
+ Future saveUserName(String login) async {
+ await _hiveBox?.put(_USER_NAME_KEY, login);
+ }
+
+ Future saveUserPassword(String password) async {
+ await _hiveBox?.put(_USER_PASSWORD_KEY, password);
+ }
+
+ Future isExistSavedUser() async {
+ int userId = await _hiveBox?.get(_USER_ID_KEY) ?? _NOT_SAVED_USER_ID;
+
+ return userId > _NOT_SAVED_USER_ID;
+ }
+
+ Future cleanCredentials() async {
+ await _hiveBox?.clear();
+ }
+}
diff --git a/videocall_webrtc_sample/lib/managers/users_manager.dart b/videocall_webrtc_sample/lib/managers/users_manager.dart
new file mode 100644
index 0000000..f903b7c
--- /dev/null
+++ b/videocall_webrtc_sample/lib/managers/users_manager.dart
@@ -0,0 +1,24 @@
+import 'package:quickblox_sdk/models/qb_filter.dart';
+import 'package:quickblox_sdk/models/qb_sort.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:quickblox_sdk/quickblox_sdk.dart';
+import 'package:quickblox_sdk/users/constants.dart';
+
+class UsersManager {
+ Future> getUsers(int page, int perPage,
+ {QBSort? sort, QBFilter? filter}) async {
+ return QB.users
+ .getUsers(sort: sort, page: page, perPage: perPage, filter: filter);
+ }
+
+ Future> getUsersByIds(List? userIds) async {
+ String? filterValue = userIds?.join(",");
+ QBFilter filter = QBFilter();
+ filter.field = QBUsersFilterFields.ID;
+ filter.operator = QBUsersFilterOperators.IN;
+ filter.value = filterValue;
+ filter.type = QBUsersFilterTypes.STRING;
+
+ return await QB.users.getUsers(filter: filter);
+ }
+}
diff --git a/videocall_webrtc_sample/lib/mappers/qb_rtc_session_mapper.dart b/videocall_webrtc_sample/lib/mappers/qb_rtc_session_mapper.dart
new file mode 100644
index 0000000..1106bc0
--- /dev/null
+++ b/videocall_webrtc_sample/lib/mappers/qb_rtc_session_mapper.dart
@@ -0,0 +1,31 @@
+import 'package:quickblox_sdk/models/qb_rtc_session.dart';
+
+class QBRTCSessionMapper {
+ static QBRTCSession? mapToQBRtcSession(Map? map) {
+ if (map == null || map.isEmpty) {
+ return null;
+ }
+
+ QBRTCSession qbrtcSession = QBRTCSession();
+
+ if (map.containsKey("id")) {
+ qbrtcSession.id = map["id"] as String?;
+ }
+ if (map.containsKey("type")) {
+ qbrtcSession.type = map["type"] as int?;
+ }
+ if (map.containsKey("state")) {
+ qbrtcSession.state = map["state"] as int?;
+ }
+ if (map.containsKey("initiatorId")) {
+ qbrtcSession.initiatorId = map["initiatorId"] as int?;
+ }
+ if (map.containsKey("opponentsIds")) {
+ List opponentIdsList =
+ List.from(map["opponentsIds"] as Iterable);
+ qbrtcSession.opponentsIds = opponentIdsList;
+ }
+
+ return qbrtcSession;
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/base_view_model.dart b/videocall_webrtc_sample/lib/presentation/base_view_model.dart
new file mode 100644
index 0000000..3de14e1
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/base_view_model.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/cupertino.dart';
+
+class BaseViewModel extends ChangeNotifier {
+ bool _loading = false;
+
+ bool get loading => _loading;
+
+ String? _errorMessage;
+
+ String? get errorMessage => _errorMessage;
+
+ void showLoading() {
+ if (!_loading) {
+ _loading = true;
+ notifyListeners();
+ }
+ }
+
+ void hideLoading() {
+ if (_loading) {
+ _loading = false;
+ notifyListeners();
+ }
+ }
+
+ void showError(String errorMessage) {
+ _errorMessage = errorMessage;
+ notifyListeners();
+ }
+
+ void hideError() {
+ _errorMessage = null;
+ notifyListeners();
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/dialogs/base_dialog.dart b/videocall_webrtc_sample/lib/presentation/dialogs/base_dialog.dart
new file mode 100644
index 0000000..ad218b6
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/dialogs/base_dialog.dart
@@ -0,0 +1,35 @@
+import 'package:flutter/material.dart';
+
+abstract class BaseDialog {
+ List getWidgets(BuildContext context);
+
+ void show(BuildContext context,
+ {String title = "", bool dismissTouchOutside = false}) {
+ List widgets = getWidgets(context);
+
+ Widget dialog = _build(widgets, title);
+
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ showDialog(
+ barrierDismissible: dismissTouchOutside,
+ context: context,
+ builder: (_) => dialog);
+ });
+ }
+
+ Widget _build(List widgets, String title) {
+ return AlertDialog(
+ title: _buildTitle(title),
+ shape: _buildShapeBorder(),
+ actions: widgets);
+ }
+
+ Widget _buildTitle(String title) {
+ return Text(title);
+ }
+
+ ShapeBorder _buildShapeBorder() {
+ return const RoundedRectangleBorder(
+ borderRadius: BorderRadius.all(Radius.circular(10)));
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/dialogs/button_dialog.dart b/videocall_webrtc_sample/lib/presentation/dialogs/button_dialog.dart
new file mode 100644
index 0000000..8630649
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/dialogs/button_dialog.dart
@@ -0,0 +1,9 @@
+import 'package:flutter/material.dart';
+import 'base_dialog.dart';
+
+abstract class ButtonDialog extends BaseDialog {
+ @protected
+ Widget buildButton(String title, {VoidCallback? onPressed}) {
+ return TextButton(onPressed: onPressed, child: Text(title));
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/dialogs/yes_no_dialog.dart b/videocall_webrtc_sample/lib/presentation/dialogs/yes_no_dialog.dart
new file mode 100644
index 0000000..497ad53
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/dialogs/yes_no_dialog.dart
@@ -0,0 +1,37 @@
+import 'package:flutter/cupertino.dart';
+
+import 'button_dialog.dart';
+
+class YesNoDialog extends ButtonDialog {
+ final VoidCallback onPressedPrimary;
+ final VoidCallback onPressedSecondary;
+ final String primaryButtonText;
+ final String secondaryButtonText;
+
+ YesNoDialog(
+ {required this.onPressedPrimary,
+ required this.onPressedSecondary,
+ required this.primaryButtonText,
+ required this.secondaryButtonText});
+
+ @override
+ List getWidgets(BuildContext context) {
+ return [
+ _buildNoButton(secondaryButtonText, context),
+ _buildYesButton(primaryButtonText, context)
+ ];
+ }
+
+ Widget _buildYesButton(String title, BuildContext context) {
+ return _buildButton(title, context, onPressedPrimary);
+ }
+
+ Widget _buildNoButton(String title, BuildContext context) {
+ return _buildButton(title, context, onPressedSecondary);
+ }
+
+ Widget _buildButton(
+ String title, BuildContext context, VoidCallback onPressed) {
+ return buildButton(title, onPressed: onPressed);
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/audio/audio_call/audio_call_screen.dart b/videocall_webrtc_sample/lib/presentation/screens/call/audio/audio_call/audio_call_screen.dart
new file mode 100644
index 0000000..26e76ad
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/audio/audio_call/audio_call_screen.dart
@@ -0,0 +1,99 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/audio/audio_call/widgets/controls_audio_call_buttons.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/widgets/timer_widget.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/widgets/user_avatars.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/widgets/user_names.dart';
+
+import '../../../../../managers/call_manager.dart';
+import '../../../../utils/notification_utils.dart';
+import 'audio_call_screen_view_model.dart';
+
+class AudioCallScreen extends StatelessWidget {
+ static show(BuildContext context, bool isIncoming, List opponents) {
+ return Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => AudioCallScreen(isIncoming: isIncoming, opponents: opponents)));
+ }
+
+ final AudioCallScreenViewModel _viewModel = AudioCallScreenViewModel();
+
+ AudioCallScreen({super.key, required bool isIncoming, required List opponents}) {
+ _viewModel.init(isIncoming, opponents);
+ }
+
+ @override
+ Widget build(context) {
+ return PopScope(
+ canPop: false,
+ child: ChangeNotifierProvider(
+ create: (context) => _viewModel,
+ child: Scaffold(
+ backgroundColor: const Color(0xFF414E5B),
+ body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
+ UserAvatars(opponentsLength: _viewModel.getOpponentsLength()),
+ _buildTextCalling(),
+ Selector(
+ selector: (_, viewModel) => viewModel.isStartedCall,
+ builder: (_, isStarted, __) {
+ return isStarted ? const TimerWidget() : const SizedBox.shrink();
+ }),
+ UserNames(users: _viewModel.opponents ?? []),
+ SizedBox(height: MediaQuery.of(context).size.height / 2 - 120),
+ ControlsAudioCallButtons(
+ onEndCall: () => _viewModel.hangUpCall(),
+ onMute: (isPressed) {
+ bool isNeedMute = !isPressed;
+ _viewModel.enableAudio(isNeedMute);
+ },
+ onSpeaker: (isPressed) {
+ if (isPressed) {
+ _viewModel.switchAudioOutput(AudioOutputTypes.LOUDSPEAKER);
+ } else {
+ _viewModel.switchAudioOutput(AudioOutputTypes.EAR_SPEAKER);
+ }
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.errorMessage,
+ builder: (_, errorMessage, __) {
+ if (errorMessage == null) {
+ _hideErrorSnackBar(context);
+ }
+ if (errorMessage?.isNotEmpty ?? false) {
+ _showErrorSnackBar(errorMessage!, context);
+ }
+ return const SizedBox.shrink();
+ }),
+ Selector(
+ selector: (_, viewModel) => viewModel.isEndCall,
+ builder: (_, isCallEnd, __) {
+ if (isCallEnd) {
+ WidgetsBinding.instance
+ .addPostFrameCallback((_) => Navigator.of(context).pop());
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ ]))));
+ }
+
+ Widget _buildTextCalling() {
+ return const Padding(
+ padding: EdgeInsets.symmetric(vertical: 8.0),
+ child: Text('Calling to',
+ style:
+ TextStyle(fontSize: 14.0, fontWeight: FontWeight.w500, color: Color(0xFF90979F))));
+ }
+
+ void _showErrorSnackBar(String errorMessage, BuildContext context,
+ {VoidCallback? errorCallback}) {
+ return NotificationUtils.showSnackBarError(context, errorMessage, errorCallback: errorCallback);
+ }
+
+ void _hideErrorSnackBar(BuildContext context) {
+ return NotificationUtils.hideSnackBar(context);
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/audio/audio_call/audio_call_screen_view_model.dart b/videocall_webrtc_sample/lib/presentation/screens/call/audio/audio_call/audio_call_screen_view_model.dart
new file mode 100644
index 0000000..8ae5b89
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/audio/audio_call/audio_call_screen_view_model.dart
@@ -0,0 +1,120 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:videocall_webrtc_sample/dependency/dependency_impl.dart';
+import 'package:videocall_webrtc_sample/managers/call_manager.dart';
+
+import '../../../../../managers/callback/call_subscription.dart';
+import '../../../../../managers/callback/call_subscription_impl.dart';
+import '../../../../../managers/ringtone_manager.dart';
+import '../../../../base_view_model.dart';
+import '../../../../utils/error_parser.dart';
+
+class AudioCallScreenViewModel extends BaseViewModel {
+ final CallManager _callManager = DependencyImpl.getInstance().getCallManager();
+ final RingtoneManager _ringtoneManager = DependencyImpl.getInstance().getRingtoneManager();
+
+ List? _opponents;
+ List? get opponents => _opponents;
+
+ bool _isStartedCall = false;
+
+ bool get isStartedCall => _isStartedCall;
+
+ bool _isEndCall = false;
+
+ bool get isEndCall => _isEndCall;
+
+ String? callAcceptErrorMessage;
+
+ CallSubscription? _callSubscription;
+
+ Future init(bool isIncoming, List opponents) async {
+ _opponents = opponents;
+
+ _callSubscription = _createCallSubscription();
+ await _subscribeCall();
+
+ if (isIncoming) {
+ _isStartedCall = true;
+ notifyListeners();
+ } else {
+ _ringtoneManager.startBeeps();
+ }
+ }
+
+ int getOpponentsLength() {
+ return _opponents?.length ?? 0;
+ }
+
+ CallSubscription _createCallSubscription() {
+ return CallSubscriptionImpl(
+ tag: "AudioCallScreenViewModel",
+ onCallEnd: () {
+ _ringtoneManager.release();
+ _isEndCall = true;
+ notifyListeners();
+ },
+ onCallAccept: (opponentId) {
+ _ringtoneManager.release();
+ _isStartedCall = true;
+ notifyListeners();
+ },
+ onError: (errorMessage) {
+ _showError(errorMessage);
+ });
+ }
+
+ @override
+ void dispose() {
+ _unsubscribeCall();
+ super.dispose();
+ }
+
+ Future _subscribeCall() async {
+ try {
+ await _callManager.subscribeCall(_callSubscription);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future _unsubscribeCall() async {
+ try {
+ await _callManager.unsubscribeCall(_callSubscription);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ void _showError(String errorMessage) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ callAcceptErrorMessage = errorMessage;
+ notifyListeners();
+ });
+ }
+
+ Future enableAudio(bool enable) async {
+ try {
+ await _callManager.enableAudio(enable);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future switchAudioOutput(AudioOutputTypes outputType) async {
+ try {
+ await _callManager.switchAudio(outputType);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future hangUpCall() async {
+ try {
+ await _callManager.hangUpCall();
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/audio/audio_call/widgets/controls_audio_call_buttons.dart b/videocall_webrtc_sample/lib/presentation/screens/call/audio/audio_call/widgets/controls_audio_call_buttons.dart
new file mode 100644
index 0000000..72965b1
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/audio/audio_call/widgets/controls_audio_call_buttons.dart
@@ -0,0 +1,45 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+
+import '../../../widgets/circular_button.dart';
+import '../../../widgets/circular_button_with_state.dart';
+
+class ControlsAudioCallButtons extends StatelessWidget {
+ final void Function(bool isPressed) _onMute;
+ final void Function(bool isPressed) _onSpeaker;
+ final void Function() _onEndCall;
+
+ const ControlsAudioCallButtons(
+ {super.key, required onMute, required onSpeaker, required onEndCall})
+ : _onMute = onMute,
+ _onSpeaker = onSpeaker,
+ _onEndCall = onEndCall;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 40.0),
+ child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
+ CircularButtonWithState(
+ onPressed: (isPressed) => _onMute.call(isPressed),
+ backgroundColor: const Color(0xFF202F3E),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/mute.svg', height: 35, width: 35),
+ textBelowButton: 'Mute',
+ isPressed: false),
+ CircularButton(
+ onPressed: () => _onEndCall.call(),
+ backgroundColor: const Color(0xFFFF3B30),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/hang_up.svg', height: 60, width: 60),
+ textBelowButton: 'End call'),
+ CircularButtonWithState(
+ onPressed: (isPressed) => _onSpeaker.call(isPressed),
+ backgroundColor: const Color(0xFF202F3E),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/speaker.svg', height: 60, width: 60),
+ textBelowButton: 'Speaker',
+ isPressed: false)
+ ]));
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/audio/incoming/incoming_audio_call_screen.dart b/videocall_webrtc_sample/lib/presentation/screens/call/audio/incoming/incoming_audio_call_screen.dart
new file mode 100644
index 0000000..f070a71
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/audio/incoming/incoming_audio_call_screen.dart
@@ -0,0 +1,112 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/audio/incoming/widgets/controls_incom_audio_buttons.dart';
+
+import '../../../../utils/notification_utils.dart';
+import '../../widgets/user_avatars.dart';
+import '../../widgets/user_names.dart';
+import '../audio_call/audio_call_screen.dart';
+import 'incoming_audio_call_screen_view_model.dart';
+
+class IncomingAudioCallScreen extends StatefulWidget {
+ final List opponents;
+
+ const IncomingAudioCallScreen({super.key, required this.opponents});
+
+ static show(
+ BuildContext context, {
+ required List opponents,
+ }) {
+ return Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => IncomingAudioCallScreen(
+ opponents: opponents,
+ )));
+ }
+
+ @override
+ State createState() => _IncomingAudioCallScreenState();
+}
+
+class _IncomingAudioCallScreenState extends State {
+ final IncomingAudioCallScreenViewModel _viewModel = IncomingAudioCallScreenViewModel();
+
+ @override
+ void initState() {
+ super.initState();
+ _viewModel.init(widget.opponents);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return PopScope(
+ canPop: false,
+ child: ChangeNotifierProvider(
+ create: (context) => _viewModel,
+ child: Scaffold(
+ backgroundColor: const Color(0xFF414E5B),
+ body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
+ UserAvatars(opponentsLength: _viewModel.opponents!.length),
+ UserNames(users: _viewModel.opponents!),
+ SizedBox(height: MediaQuery.of(context).size.height / 2 - 104),
+ ControlsIncomeAudioButtons(
+ onAccept: () => _viewModel.acceptCall(),
+ onReject: () => _viewModel.rejectCall(),
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.isCallEnd,
+ builder: (_, isCallEnd, __) {
+ if (isCallEnd) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ Navigator.of(context).pop();
+ });
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.callAccepted,
+ builder: (_, callAccepted, __) {
+ if (callAccepted) {
+ _removeCurrentScreenAndShowAudioCallScreen();
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.callRejected,
+ builder: (_, callRejected, __) {
+ if (callRejected) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ Navigator.of(context).pop();
+ });
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.errorMessage,
+ builder: (_, errorMessage, __) {
+ if (errorMessage?.isNotEmpty ?? false) {
+ _showErrorSnackbar(errorMessage!, context);
+ }
+ return const SizedBox.shrink();
+ }),
+ ]))));
+ }
+
+ Future _removeCurrentScreenAndShowAudioCallScreen() async {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ Navigator.of(context).pop();
+ List opponents = _viewModel.opponents!;
+ AudioCallScreen.show(context, true, opponents);
+ });
+ }
+
+ void _showErrorSnackbar(String errorMessage, BuildContext context,
+ {VoidCallback? errorCallback}) {
+ return NotificationUtils.showSnackBarError(context, errorMessage, errorCallback: errorCallback);
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/audio/incoming/incoming_audio_call_screen_view_model.dart b/videocall_webrtc_sample/lib/presentation/screens/call/audio/incoming/incoming_audio_call_screen_view_model.dart
new file mode 100644
index 0000000..25d6cd2
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/audio/incoming/incoming_audio_call_screen_view_model.dart
@@ -0,0 +1,109 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:videocall_webrtc_sample/managers/call_manager.dart';
+import 'package:videocall_webrtc_sample/managers/users_manager.dart';
+import 'package:videocall_webrtc_sample/presentation/utils/error_parser.dart';
+
+import '../../../../../dependency/dependency_impl.dart';
+import '../../../../../managers/callback/call_subscription.dart';
+import '../../../../../managers/callback/call_subscription_impl.dart';
+import '../../../../../managers/ringtone_manager.dart';
+import '../../../../../managers/storage_manager.dart';
+import '../../../../base_view_model.dart';
+
+class IncomingAudioCallScreenViewModel extends BaseViewModel {
+ final RingtoneManager _ringtoneManager = DependencyImpl.getInstance().getRingtoneManager();
+ final CallManager _callManager = DependencyImpl.getInstance().getCallManager();
+ final UsersManager _usersManager = DependencyImpl.getInstance().getUsersManager();
+ final StorageManager _storageManager = DependencyImpl.getInstance().getStorageManager();
+
+ List? opponents;
+
+ bool callAccepted = false;
+ bool callRejected = false;
+ bool isCallEnd = false;
+ CallSubscription? _callSubscription;
+
+ Future init(List? opponents) async {
+ try {
+ _ringtoneManager.startRingtone();
+ } on PlatformException catch (e) {
+ _showIncomeError(ErrorParser.parseFrom(e));
+ }
+ _callSubscription = _createCallSubscription();
+ _subscribeCall();
+ this.opponents = opponents;
+ }
+
+ @override
+ void dispose() {
+ _unsubscribeCall();
+ super.dispose();
+ }
+
+ Future _subscribeCall() async {
+ try {
+ await _callManager.subscribeCall(_callSubscription);
+ } on PlatformException catch (e) {
+ _showIncomeError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future _unsubscribeCall() async {
+ try {
+ await _callManager.unsubscribeCall(_callSubscription);
+ } on PlatformException catch (e) {
+ _showIncomeError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future acceptCall() async {
+ try {
+ await _ringtoneManager.release();
+ await _callManager.acceptCall();
+ callAccepted = true;
+ notifyListeners();
+ } on PlatformException catch (e) {
+ _showIncomeError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future rejectCall() async {
+ try {
+ await _callManager.rejectCall();
+ } on PlatformException catch (e) {
+ _showIncomeError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ void _showIncomeError(String errorMessage) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ showError(errorMessage);
+ });
+ }
+
+ void loadOpponents(List? opponentsIds) async {
+ try {
+ int currentUserId = await _storageManager.getLoggedUserId();
+
+ opponentsIds?.remove(currentUserId);
+ opponents = await _usersManager.getUsersByIds(opponentsIds);
+ } on PlatformException catch (e) {
+ _showIncomeError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ CallSubscription _createCallSubscription() {
+ return CallSubscriptionImpl(
+ tag: "IncomingCallScreenViewModel",
+ onCallEnd: () {
+ _ringtoneManager.release();
+ isCallEnd = true;
+ notifyListeners();
+ },
+ onError: (errorMessage) {
+ _showIncomeError(errorMessage);
+ });
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/audio/incoming/widgets/controls_incom_audio_buttons.dart b/videocall_webrtc_sample/lib/presentation/screens/call/audio/incoming/widgets/controls_incom_audio_buttons.dart
new file mode 100644
index 0000000..8ba981e
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/audio/incoming/widgets/controls_incom_audio_buttons.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+
+import '../../../widgets/circular_button.dart';
+
+class ControlsIncomeAudioButtons extends StatelessWidget {
+ final void Function() _onReject;
+ final void Function() _onAccept;
+
+ const ControlsIncomeAudioButtons({
+ super.key,
+ required void Function() onReject,
+ required void Function() onAccept,
+ }) : _onReject = onReject,
+ _onAccept = onAccept;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 40.0),
+ child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
+ CircularButton(
+ onPressed: () => _onReject.call(),
+ backgroundColor: const Color(0xFFFF3B30),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/hang_up.svg', height: 35, width: 35),
+ textBelowButton: 'Decline'),
+ CircularButton(
+ onPressed: () => _onAccept.call(),
+ backgroundColor: const Color(0xFF49CF77),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/accept.svg', height: 60, width: 60),
+ textBelowButton: 'Accept')
+ ]));
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/video/incoming/incoming_video_call_screen.dart b/videocall_webrtc_sample/lib/presentation/screens/call/video/incoming/incoming_video_call_screen.dart
new file mode 100644
index 0000000..332e638
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/video/incoming/incoming_video_call_screen.dart
@@ -0,0 +1,90 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/video/incoming/widgets/income_video_buttons.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/video/video_call/video_call_screen.dart';
+
+import '../../../../utils/notification_utils.dart';
+import '../../widgets/user_avatars.dart';
+import '../../widgets/user_names.dart';
+import 'incoming_video_call_screen_view_model.dart';
+
+class IncomingVideoCallScreen extends StatelessWidget {
+ static show(BuildContext context, List opponents) {
+ return Navigator.push(context, MaterialPageRoute(builder: (_) => IncomingVideoCallScreen(opponents: opponents)));
+ }
+
+ final IncomingVideoCallScreenViewModel _viewModel = IncomingVideoCallScreenViewModel();
+
+ IncomingVideoCallScreen({super.key, required List opponents}) {
+ _viewModel.init(opponents);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return PopScope(
+ canPop: false,
+ child: ChangeNotifierProvider(
+ create: (context) => _viewModel,
+ child: Scaffold(
+ backgroundColor: const Color(0xFF414E5B),
+ body: Column(mainAxisAlignment: MainAxisAlignment.center, children: [
+ UserAvatars(opponentsLength: _viewModel.getOpponentsCount()),
+ UserNames(users: _viewModel.users),
+ SizedBox(height: MediaQuery.of(context).size.height / 2 - 104),
+ IncomeVideoButtons(
+ onReject: () => _viewModel.rejectCall(),
+ onAccept: () => _viewModel.acceptCall(),
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.isCallEnd,
+ builder: (_, isCallEnd, __) {
+ if (isCallEnd) {
+ WidgetsBinding.instance.addPostFrameCallback((_) => Navigator.of(context).pop());
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.isCallAccepted,
+ builder: (_, callAccepted, __) {
+ if (callAccepted) {
+ _removeCurrentScreenAndShowVideoCallScreen(context);
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.isCallRejected,
+ builder: (_, callRejected, __) {
+ if (callRejected) {
+ WidgetsBinding.instance.addPostFrameCallback((_) => Navigator.of(context).pop());
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.errorMessage,
+ builder: (_, errorMessage, __) {
+ if (errorMessage == null) {
+ NotificationUtils.hideSnackBar(context);
+ }
+ if (errorMessage?.isNotEmpty ?? false) {
+ _showErrorSnackBar(errorMessage!, context);
+ }
+ return const SizedBox.shrink();
+ }),
+ ]))));
+ }
+
+ Future _removeCurrentScreenAndShowVideoCallScreen(BuildContext context) async {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ Navigator.of(context).pop();
+ VideoCallScreen.show(context, true, _viewModel.users);
+ });
+ }
+
+ void _showErrorSnackBar(String errorMessage, BuildContext context, {VoidCallback? errorCallback}) {
+ return NotificationUtils.showSnackBarError(context, errorMessage, errorCallback: errorCallback);
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/video/incoming/incoming_video_call_screen_view_model.dart b/videocall_webrtc_sample/lib/presentation/screens/call/video/incoming/incoming_video_call_screen_view_model.dart
new file mode 100644
index 0000000..e1527bb
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/video/incoming/incoming_video_call_screen_view_model.dart
@@ -0,0 +1,107 @@
+import 'package:flutter/cupertino.dart';
+import 'package:flutter/services.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:videocall_webrtc_sample/managers/call_manager.dart';
+import 'package:videocall_webrtc_sample/managers/ringtone_manager.dart';
+
+import '../../../../../dependency/dependency_impl.dart';
+import '../../../../../managers/callback/call_subscription.dart';
+import '../../../../../managers/callback/call_subscription_impl.dart';
+import '../../../../base_view_model.dart';
+import '../../../../utils/error_parser.dart';
+
+class IncomingVideoCallScreenViewModel extends BaseViewModel {
+ final RingtoneManager _ringtoneManager = DependencyImpl.getInstance().getRingtoneManager();
+ final CallManager _callManager = DependencyImpl.getInstance().getCallManager();
+
+ final List _users = [];
+
+ List get users => _users;
+
+ bool isCallAccepted = false;
+ bool isCallRejected = false;
+ bool isCallEnd = false;
+
+ CallSubscription? _callSubscription;
+
+ Future init(List callUsers) async {
+ try {
+ _ringtoneManager.startRingtone();
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+
+ _callSubscription = _createCallSubscription();
+ _subscribeCall();
+ _users.addAll(callUsers);
+ }
+
+ CallSubscription _createCallSubscription() {
+ return CallSubscriptionImpl(
+ tag: "IncomingCallScreenViewModel",
+ onCallEnd: () {
+ _ringtoneManager.release();
+ isCallEnd = true;
+ notifyListeners();
+ },
+ onError: (errorMessage) {
+ _showError(errorMessage);
+ });
+ }
+
+ int getOpponentsCount() {
+ return users.length;
+ }
+
+ Future _subscribeCall() async {
+ try {
+ await _callManager.subscribeCall(_callSubscription);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future _unsubscribeCall() async {
+ try {
+ await _callManager.unsubscribeCall(_callSubscription);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future acceptCall() async {
+ try {
+ await _ringtoneManager.release();
+ isCallAccepted = true;
+ notifyListeners();
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future enableVideo(bool enable) async {
+ try {
+ await _callManager.enableVideo(enable);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future rejectCall() async {
+ try {
+ await _callManager.rejectCall();
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ void _showError(String errorMessage) {
+ WidgetsBinding.instance.addPostFrameCallback((_) => showError(errorMessage));
+ }
+
+ @override
+ void dispose() {
+ _unsubscribeCall();
+ super.dispose();
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/video/incoming/widgets/income_video_buttons.dart b/videocall_webrtc_sample/lib/presentation/screens/call/video/incoming/widgets/income_video_buttons.dart
new file mode 100644
index 0000000..16b3d5d
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/video/incoming/widgets/income_video_buttons.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+
+import '../../../widgets/circular_button.dart';
+
+class IncomeVideoButtons extends StatelessWidget {
+ final void Function() _onReject;
+ final void Function() _onAccept;
+
+ const IncomeVideoButtons({
+ super.key,
+ required void Function() onReject,
+ required void Function() onAccept,
+ }) : _onReject = onReject,
+ _onAccept = onAccept;
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 40.0),
+ child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
+ CircularButton(
+ onPressed: () => _onReject.call(),
+ backgroundColor: const Color(0xFFFF3B30),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/hang_up.svg', height: 35, width: 35),
+ textBelowButton: 'Decline'),
+ CircularButton(
+ onPressed: () => _onAccept.call(),
+ backgroundColor: const Color(0xFF49CF77),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/video.svg', height: 60, width: 60),
+ textBelowButton: 'Video')
+ ]));
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/video_call_screen.dart b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/video_call_screen.dart
new file mode 100644
index 0000000..f59bcec
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/video_call_screen.dart
@@ -0,0 +1,87 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/video/video_call/video_call_screen_view_model.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/video/video_call/widgets/controls_video_call_buttons.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/video/video_call/widgets/video_call_app_bar.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/video/video_call/widgets/video_tracks_widget.dart';
+
+import '../../../../utils/notification_utils.dart';
+
+class VideoCallScreen extends StatelessWidget {
+ static show(BuildContext context, bool isIncoming, List callUsers) {
+ return Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => VideoCallScreen(isIncoming: isIncoming, callUsers: callUsers)));
+ }
+
+ final List callUsers;
+ final bool isIncoming;
+
+ final VideoCallScreenViewModel _viewModel = VideoCallScreenViewModel();
+
+ VideoCallScreen({
+ super.key,
+ required this.isIncoming,
+ required this.callUsers,
+ }) {
+ _viewModel.init(isIncoming, callUsers);
+ }
+
+ @override
+ Widget build(context) {
+ return PopScope(
+ canPop: false,
+ child: ChangeNotifierProvider(
+ create: (context) => _viewModel,
+ child: Container(
+ color: const Color(0xFF414E5B),
+ child: Stack(children: [
+ VideoTracksWidget(_viewModel.getVideoCallEntities()),
+ VideoCallAppBar(text: _viewModel.getOpponentNames(callUsers)),
+ VideoCallButtons(
+ onMute: (isPressed) {
+ bool isNeedMute = !isPressed;
+ _viewModel.enableAudio(isNeedMute);
+ },
+ onDisableCamera: (isPressed) {
+ bool isNeedDisable = !isPressed;
+ _viewModel.enableVideo(isNeedDisable);
+ },
+ onSwitchCamera: () => _viewModel.switchCamera(),
+ onEndCall: () => _viewModel.hangUpCall()),
+ Selector(
+ selector: (_, viewModel) => viewModel.opponentActionMessage,
+ builder: (_, message, __) {
+ if (message?.isNotEmpty ?? false) {
+ NotificationUtils.showResult(context, message!);
+ }
+ return const SizedBox.shrink();
+ }),
+ Selector(
+ selector: (_, viewModel) => viewModel.isEndCall,
+ builder: (_, isCallEnd, __) {
+ if (isCallEnd) {
+ WidgetsBinding.instance
+ .addPostFrameCallback((_) => Navigator.of(context).pop());
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.errorMessage,
+ builder: (_, errorMessage, __) {
+ if (errorMessage == null) {
+ NotificationUtils.hideSnackBar(context);
+ }
+ if (errorMessage?.isNotEmpty ?? false) {
+ NotificationUtils.showSnackBarError(context, errorMessage!);
+ }
+ return const SizedBox.shrink();
+ })
+ ]))));
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/video_call_screen_view_model.dart b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/video_call_screen_view_model.dart
new file mode 100644
index 0000000..8d43a7c
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/video_call_screen_view_model.dart
@@ -0,0 +1,217 @@
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:videocall_webrtc_sample/dependency/dependency_impl.dart';
+import 'package:videocall_webrtc_sample/entities/video_call_entity.dart';
+import 'package:videocall_webrtc_sample/managers/call_manager.dart';
+import 'package:videocall_webrtc_sample/managers/callback/call_subscription.dart';
+import 'package:videocall_webrtc_sample/managers/callback/call_subscription_impl.dart';
+import 'package:videocall_webrtc_sample/managers/ringtone_manager.dart';
+import 'package:videocall_webrtc_sample/managers/storage_manager.dart';
+import 'package:videocall_webrtc_sample/presentation/utils/error_parser.dart';
+
+import '../../../../base_view_model.dart';
+
+class VideoCallScreenViewModel extends BaseViewModel {
+ final CallManager _callManager = DependencyImpl.getInstance().getCallManager();
+ final RingtoneManager _ringtoneManager = DependencyImpl.getInstance().getRingtoneManager();
+ final StorageManager _storageManager = DependencyImpl.getInstance().getStorageManager();
+
+ String? _opponentActionMessage;
+
+ String? get opponentActionMessage => _opponentActionMessage;
+
+ bool _isStartedCall = false;
+
+ bool get isStartedCall => _isStartedCall;
+
+ bool _isEndCall = false;
+
+ bool get isEndCall => _isEndCall;
+
+ CallSubscription? _callSubscription;
+
+ Future init(bool isIncoming, List users) async {
+ _callSubscription = _createCallSubscription(users);
+ await _subscribeCall();
+
+ if (isIncoming) {
+ await startIncomingCall();
+ } else {
+ await startOutgoingCall(users);
+ }
+
+ await _callManager.switchAudio(AudioOutputTypes.LOUDSPEAKER);
+ }
+
+ Future startIncomingCall() async {
+ _isStartedCall = true;
+ await _callManager.acceptCall();
+
+ notifyListeners();
+ }
+
+ Future startOutgoingCall(List users) async {
+ int loggedUserId = await _storageManager.getLoggedUserId();
+ List opponents = await removeLoggedUserFrom(users, loggedUserId);
+
+ await startVideoCall(opponents);
+ await _ringtoneManager.startBeeps();
+ }
+
+ Future> removeLoggedUserFrom(List users, int loggedUserId) async {
+ try {
+ QBUser? loggedUser = users.firstWhere((element) => element?.id == loggedUserId);
+ users.remove(loggedUser);
+ return users;
+ } on StateError catch (e) {
+ print(e.message);
+ }
+ return users;
+ }
+
+ List getVideoCallEntities() {
+ return _callManager.videoCallEntities.toList();
+ }
+
+ String getOpponentNames(List users) {
+ bool isNotExistOpponents = users.length < 2;
+ if (isNotExistOpponents) {
+ return "UserName";
+ }
+
+ bool isOnlyOneOpponent = users.length == 2;
+ if (isOnlyOneOpponent) {
+ String opponentNames = users[1]?.fullName ?? users[1]?.login ?? "";
+ return opponentNames;
+ }
+
+ String opponentNames = "";
+ for (var i = 1; i < users.length; i++) {
+ opponentNames = '$opponentNames, ${users[i]?.fullName ?? users[i]?.login}';
+ }
+
+ return opponentNames;
+ }
+
+ CallSubscription _createCallSubscription(List users) {
+ return CallSubscriptionImpl(
+ tag: "AudioCallScreenViewModel",
+ onCallEnd: () {
+ _ringtoneManager.release();
+ _isEndCall = true;
+ notifyListeners();
+ },
+ onCallAccept: (userId) {
+ _ringtoneManager.release();
+ _isStartedCall = true;
+ notifyListeners();
+ },
+ onNotAnswer: (userId) {
+ String? name = getUserNameBy(users, userId);
+ _showOpponentActionMessage("$name is not answering.");
+ },
+ onHangup: (userId) {
+ String? name = getUserNameBy(users, userId);
+ _showOpponentActionMessage("$name hung up.");
+ },
+ onCallRejected: (userId) {
+ String? name = getUserNameBy(users, userId);
+ _showOpponentActionMessage("$name rejected the call.");
+ },
+ onError: (errorMessage) {
+ _showError(errorMessage);
+ },
+ );
+ }
+
+ String? getUserNameBy(List users, int userId) {
+ try {
+ QBUser? user = users.firstWhere((element) => element?.id == userId);
+ return user?.fullName ?? user?.login;
+ } on StateError catch (e) {
+ return "UserName";
+ }
+ }
+
+ Future _subscribeCall() async {
+ try {
+ await _callManager.subscribeCall(_callSubscription);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future _unsubscribeCall() async {
+ try {
+ await _callManager.unsubscribeCall(_callSubscription);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future startVideoCall(List users) async {
+ try {
+ await _callManager.startVideoCall(users);
+ } on PlatformException catch (e) {
+ showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future enableAudio(bool enable) async {
+ try {
+ await _callManager.enableAudio(enable);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future enableVideo(bool enable) async {
+ try {
+ await _callManager.enableVideo(enable);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future switchAudioOutput(AudioOutputTypes outputType) async {
+ try {
+ await _callManager.switchAudio(outputType);
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future switchCamera() async {
+ try {
+ await _callManager.switchCamera();
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future hangUpCall() async {
+ try {
+ await _callManager.hangUpCall();
+ } on PlatformException catch (e) {
+ _showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ void _showOpponentActionMessage(String message) {
+ _opponentActionMessage = message;
+ notifyListeners();
+ }
+
+ void _showError(String errorMessage) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ showError(errorMessage);
+ });
+ }
+
+ @override
+ void dispose() {
+ _unsubscribeCall();
+ super.dispose();
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/controls_video_call_buttons.dart b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/controls_video_call_buttons.dart
new file mode 100644
index 0000000..c3060f8
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/controls_video_call_buttons.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/widgets/circular_button.dart';
+
+import '../../../widgets/circular_button_with_state.dart';
+import 'oval_badge.dart';
+
+class VideoCallButtons extends StatelessWidget {
+ final void Function(bool isPressed) _onMute;
+ final void Function(bool isPressed) _onDisableCamera;
+ final void Function() _onSwitchCamera;
+ final void Function() _onEndCall;
+
+ const VideoCallButtons(
+ {super.key, required onMute, required onDisableCamera, required onSwitchCamera, required onEndCall})
+ : _onMute = onMute,
+ _onDisableCamera = onDisableCamera,
+ _onSwitchCamera = onSwitchCamera,
+ _onEndCall = onEndCall;
+
+ @override
+ Widget build(BuildContext context) {
+ final screenWidth = MediaQuery.of(context).size.width;
+ final screenHeight = MediaQuery.of(context).size.height;
+ return Positioned(
+ bottom: 0,
+ child: Container(
+ height: screenHeight / 4,
+ width: screenWidth,
+ color: Colors.black.withOpacity(0.5),
+ child: Column(
+ children: [
+ const OvalBadge(),
+ Padding(
+ padding: const EdgeInsets.only(left: 16, top: 16, right: 16),
+ child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
+ CircularButtonWithState(
+ onPressed: (isPressed) => _onMute.call(isPressed),
+ backgroundColor: const Color(0xFF202F3E),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/mute.svg', height: 35, width: 35),
+ textBelowButton: 'Mute',
+ isPressed: false),
+ CircularButtonWithState(
+ onPressed: (isPressed) => _onDisableCamera.call(isPressed),
+ backgroundColor: const Color(0xFF202F3E),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/enable_camera.svg', height: 35, width: 35),
+ textBelowButton: 'Camera',
+ isPressed: false),
+ CircularButton(
+ onPressed: () => _onEndCall.call(),
+ backgroundColor: const Color(0xFFFF3B30),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/hang_up.svg', height: 35, width: 35),
+ textBelowButton: 'End call'),
+ CircularButtonWithState(
+ onPressed: (isPressed) => _onSwitchCamera.call(),
+ backgroundColor: const Color(0xFF202F3E),
+ iconColor: Colors.white,
+ icon: SvgPicture.asset('assets/icons/swap_camera.svg', height: 35, width: 35),
+ textBelowButton: 'Swap',
+ isPressed: false)
+ ]))
+ ],
+ )),
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/oval_badge.dart b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/oval_badge.dart
new file mode 100644
index 0000000..fe658ad
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/oval_badge.dart
@@ -0,0 +1,78 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+class OvalBadge extends StatefulWidget {
+ const OvalBadge({super.key});
+
+ @override
+ _OvalBadgeState createState() => _OvalBadgeState();
+}
+
+class _OvalBadgeState extends State {
+ Timer? _timer;
+ String _formattedTime = '00:00';
+ int _hours = 0;
+ int _minutes = 0;
+ int _seconds = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ _timer = Timer.periodic(const Duration(seconds: 1), _updateTime);
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+
+ void _updateTime(Timer timer) {
+ _seconds++;
+ if (_seconds == 60) {
+ _seconds = 0;
+ _minutes++;
+ if (_minutes == 60) {
+ _minutes = 0;
+ _hours++;
+ }
+ }
+ setState(() {
+ if (_hours == 0) {
+ _formattedTime = '${_formatTime(_minutes)}:${_formatTime(_seconds)}';
+ } else {
+ _formattedTime = '${_formatTime(_hours)}:${_formatTime(_minutes)}:${_formatTime(_seconds)}';
+ }
+ });
+ }
+
+ String _formatTime(int time) {
+ return time.toString().padLeft(2, '0');
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(top: 24),
+ child: Container(
+ alignment: Alignment.center,
+ width: 60,
+ height: 28,
+ decoration: BoxDecoration(
+ shape: BoxShape.rectangle,
+ borderRadius: BorderRadius.circular(20),
+ color: const Color(0xFF414E5B),
+ ),
+ child: Text(
+ _formattedTime,
+ style: const TextStyle(
+ decoration: TextDecoration.none,
+ fontSize: 11,
+ color: Colors.white,
+ ),
+ ),
+ ),
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/video_call_app_bar.dart b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/video_call_app_bar.dart
new file mode 100644
index 0000000..0d9e8c3
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/video_call_app_bar.dart
@@ -0,0 +1,29 @@
+import 'package:flutter/material.dart';
+
+class VideoCallAppBar extends StatelessWidget implements PreferredSizeWidget {
+ final String text;
+
+ const VideoCallAppBar({super.key, required this.text});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ color: Colors.black.withOpacity(0.5),
+ height: 64,
+ width: MediaQuery.of(context).size.width,
+ child: Padding(
+ padding: const EdgeInsets.only(left: 16.0, bottom: 4.0),
+ child: Align(
+ alignment: FractionalOffset.bottomLeft,
+ child: Text(text,
+ textAlign: TextAlign.left,
+ style: const TextStyle(
+ decoration: TextDecoration.none,
+ fontSize: 14,
+ color: Colors.white,
+ )))));
+ }
+
+ @override
+ Size get preferredSize => const Size.fromHeight(kToolbarHeight);
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/video_track_widget.dart b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/video_track_widget.dart
new file mode 100644
index 0000000..01bb2c5
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/video_track_widget.dart
@@ -0,0 +1,49 @@
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:quickblox_sdk/webrtc/rtc_video_view.dart';
+import 'package:videocall_webrtc_sample/entities/video_call_entity.dart';
+
+class VideoTrackWidget extends StatefulWidget {
+ final VideoCallEntity _videCallEntity;
+ final double _width;
+ final double _height;
+
+ const VideoTrackWidget(
+ this._videCallEntity,
+ this._width,
+ this._height, {
+ super.key,
+ });
+
+ @override
+ State createState() => _VideoTrackWidget();
+}
+
+class _VideoTrackWidget extends State {
+ bool isVideoViewVisible = false;
+
+ @override
+ Widget build(BuildContext context) {
+ if (Platform.isAndroid) {
+ Future.delayed(const Duration(milliseconds: 2), () {
+ setState(() => isVideoViewVisible = true);
+ });
+ } else {
+ isVideoViewVisible = true;
+ }
+
+ return Container(
+ margin: const EdgeInsets.all(0.2),
+ width: widget._width,
+ height: widget._height,
+ clipBehavior: Clip.antiAlias,
+ decoration: const BoxDecoration(color: Colors.black54),
+ child: isVideoViewVisible
+ ? RTCVideoView(onVideoViewCreated: (controller) {
+ widget._videCallEntity.controller = controller;
+ })
+ : const SizedBox(),
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/video_tracks_widget.dart b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/video_tracks_widget.dart
new file mode 100644
index 0000000..ecbf865
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/video/video_call/widgets/video_tracks_widget.dart
@@ -0,0 +1,78 @@
+import 'package:flutter/material.dart';
+import 'package:tuple/tuple.dart';
+import 'package:videocall_webrtc_sample/entities/video_call_entity.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/video/video_call/widgets/video_track_widget.dart';
+
+class VideoTracksWidget extends StatelessWidget {
+ final List _videCallEntities;
+
+ const VideoTracksWidget(this._videCallEntities, {super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return _buildGrid(context);
+ }
+
+ Widget _buildGrid(BuildContext context) {
+ final rows = calcViewTable(_videCallEntities.length);
+ final totalRows = rows.item1 + rows.item2 + rows.item3;
+ final screenHeight = MediaQuery.of(context).size.height;
+ final screenWidth = MediaQuery.of(context).size.width;
+ int index = 0;
+ return Positioned(
+ top: 0,
+ left: 0,
+ child: Column(children: [
+ for (var i = 0; i < rows.item1; i++)
+ Row(children: [
+ VideoTrackWidget(_videCallEntities[index++], screenWidth, screenHeight / totalRows)
+ ]),
+ for (var i = 0; i < rows.item2; i++)
+ Row(children: [
+ for (var j = 0; j < 2; j++)
+ VideoTrackWidget(_videCallEntities[index++], screenWidth / 2, screenHeight / totalRows)
+ ]),
+ for (var i = 0; i < rows.item3; i++)
+ Row(children: [
+ for (var j = 0; j < 3; j++)
+ VideoTrackWidget(_videCallEntities[index++], screenWidth / 3, screenHeight / totalRows)
+ ])
+ ]));
+ }
+
+ Tuple3 calcViewTable(int opponentsQuantity) {
+ int rowsThreeUsers = 0;
+ int rowsTwoUsers = 0;
+ int rowsOneUser = 0;
+ if (opponentsQuantity == 1) {
+ rowsOneUser = 1;
+ return Tuple3(rowsOneUser, rowsTwoUsers, rowsThreeUsers);
+ }
+
+ if (opponentsQuantity == 2) {
+ rowsOneUser = 2;
+ return Tuple3(rowsOneUser, rowsTwoUsers, rowsThreeUsers);
+ }
+
+ if (opponentsQuantity == 3) {
+ rowsTwoUsers = 1;
+ rowsOneUser = 1;
+ return Tuple3(rowsOneUser, rowsTwoUsers, rowsThreeUsers);
+ }
+
+ switch (opponentsQuantity % 3) {
+ case 0:
+ rowsThreeUsers = opponentsQuantity ~/ 3;
+ break;
+ case 1:
+ rowsTwoUsers = 2;
+ rowsThreeUsers = (opponentsQuantity - 2) ~/ 3;
+ break;
+ case 2:
+ rowsTwoUsers = 1;
+ rowsThreeUsers = (opponentsQuantity - 1) ~/ 3;
+ break;
+ }
+ return Tuple3(rowsOneUser, rowsTwoUsers, rowsThreeUsers);
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/widgets/circular_button.dart b/videocall_webrtc_sample/lib/presentation/screens/call/widgets/circular_button.dart
new file mode 100644
index 0000000..3eb10af
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/widgets/circular_button.dart
@@ -0,0 +1,42 @@
+
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+
+class CircularButton extends StatelessWidget {
+ final VoidCallback onPressed;
+ final Color backgroundColor;
+ final Color iconColor;
+ final SvgPicture icon;
+ final String? textBelowButton;
+
+ const CircularButton(
+ {super.key,
+ required this.onPressed,
+ required this.iconColor,
+ required this.backgroundColor,
+ required this.icon,
+ this.textBelowButton});
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ children: [
+ ElevatedButton(
+ onPressed: onPressed,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: backgroundColor,
+ shape: const CircleBorder(),
+ fixedSize: const Size.fromRadius(36)),
+ child: Center(child: icon)),
+ if (textBelowButton != null)
+ Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Text(
+ textBelowButton!,
+ style: const TextStyle( decoration: TextDecoration.none,fontSize:14, color: Colors.white),
+ ),
+ )
+ ],
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/widgets/circular_button_with_state.dart b/videocall_webrtc_sample/lib/presentation/screens/call/widgets/circular_button_with_state.dart
new file mode 100644
index 0000000..33cf68c
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/widgets/circular_button_with_state.dart
@@ -0,0 +1,78 @@
+import 'dart:ui';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:flutter_svg/svg.dart';
+
+typedef IsPressedCallback = void Function(bool isPressed);
+
+class CircularButtonWithState extends StatefulWidget {
+ final IsPressedCallback onPressed;
+ final Color backgroundColor;
+ final Color iconColor;
+ final SvgPicture icon;
+ final String? textBelowButton;
+ final bool isPressed;
+
+ const CircularButtonWithState({
+ super.key,
+ required this.onPressed,
+ required this.iconColor,
+ required this.backgroundColor,
+ required this.icon,
+ this.textBelowButton,
+ required this.isPressed,
+ });
+
+ @override
+ State createState() => _CircularButtonWithState();
+}
+
+class _CircularButtonWithState extends State {
+ bool _isPressed = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _isPressed = widget.isPressed;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final backgroundColor = _isPressed ? widget.iconColor : widget.backgroundColor;
+ final iconColor = _isPressed ? widget.backgroundColor : widget.iconColor;
+
+ return Column(
+ children: [
+ ElevatedButton(
+ onPressed: () {
+ setState(() {
+ _isPressed = !_isPressed;
+ });
+ widget.onPressed(_isPressed);
+ },
+ style: ElevatedButton.styleFrom(
+ backgroundColor: backgroundColor,
+ shape: const CircleBorder(),
+ fixedSize: const Size.fromRadius(36),
+ ),
+ child: Center(
+ child: ColorFiltered(
+ colorFilter: ColorFilter.mode(
+ iconColor, // Fixed
+ BlendMode.srcATop),
+ child: widget.icon,
+ ))),
+ if (widget.textBelowButton != null)
+ Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child: Text(
+ widget.textBelowButton!,
+ style: const TextStyle(
+ decoration: TextDecoration.none,fontSize:14, color: Colors.white),
+ ),
+ )
+ ],
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/widgets/timer_widget.dart b/videocall_webrtc_sample/lib/presentation/screens/call/widgets/timer_widget.dart
new file mode 100644
index 0000000..193fda6
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/widgets/timer_widget.dart
@@ -0,0 +1,82 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+
+class TimerWidget extends StatefulWidget {
+ const TimerWidget({super.key});
+
+ @override
+ State createState() => _TimerWidgetState();
+}
+
+class _TimerWidgetState extends State {
+ Timer? _timer;
+ int _seconds = 0;
+ int _minutes = 0;
+ int _hours = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ _timer = Timer.periodic(const Duration(seconds: 1), _updateTimer);
+ }
+
+ void _updateTimer(Timer timer) {
+ setState(() {
+ _seconds++;
+ if (_seconds == 60) {
+ _seconds = 0;
+ _minutes++;
+ }
+ if (_minutes == 60) {
+ _minutes = 0;
+ _hours++;
+ }
+ });
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: const EdgeInsets.only(top: 8.0),
+ child:Row(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ _buildTimeText(_hours.toString().padLeft(2, '0')),
+ _buildDivider(),
+ _buildTimeText(_minutes.toString().padLeft(2, '0')),
+ _buildDivider(),
+ _buildTimeText(_seconds.toString().padLeft(2, '0')),
+ ],
+ )
+ );
+ }
+
+ Widget _buildTimeText(String time) {
+ return Text(
+ time,
+ style: const TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
+ );
+ }
+
+ Widget _buildDivider() {
+ return const Text(
+ ':',
+ style: TextStyle(
+ fontSize: 24,
+ fontWeight: FontWeight.bold,
+ color: Colors.white,
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/widgets/user_avatars.dart b/videocall_webrtc_sample/lib/presentation/screens/call/widgets/user_avatars.dart
new file mode 100644
index 0000000..103ef2c
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/widgets/user_avatars.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/material.dart';
+
+class UserAvatars extends StatelessWidget {
+ final int opponentsLength;
+
+ const UserAvatars({required this.opponentsLength, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Row(mainAxisAlignment: MainAxisAlignment.center, children: [
+ _buildUserAvatar(),
+ if (opponentsLength > 1)
+ Padding(padding: const EdgeInsets.only(left: 8.0), child: _buildUserAvatar())
+ ]);
+ }
+
+ Widget _buildUserAvatar() {
+ return const CircleAvatar(
+ radius: 50.0,
+ backgroundColor: Color(0xFFBCC1C5),
+ child: Icon(Icons.person_outline, size: 50.0, color: Color(0xFF636D78)));
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/call/widgets/user_names.dart b/videocall_webrtc_sample/lib/presentation/screens/call/widgets/user_names.dart
new file mode 100644
index 0000000..475b596
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/call/widgets/user_names.dart
@@ -0,0 +1,36 @@
+import 'package:flutter/material.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+
+class UserNames extends StatelessWidget {
+ final List users;
+
+ const UserNames({required this.users, super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ String firstOpponentName = "UserName";
+ if (users.isNotEmpty) {
+ firstOpponentName = users[0]?.fullName ?? "UserName";
+ }
+ return Padding(
+ padding: const EdgeInsets.only(top: 10.0),
+ child: Row(mainAxisAlignment: MainAxisAlignment.center, children: [
+ Text(firstOpponentName, style: _buildUsernameTextStyle()),
+ _buildPluralityUsersNames(),
+ ]));
+ }
+
+ TextStyle _buildUsernameTextStyle() {
+ return const TextStyle(fontSize: 20, fontWeight: FontWeight.w500, color: Colors.white);
+ }
+
+ Widget _buildPluralityUsersNames() {
+ String other = users.length == 2 ? "other" : "others";
+
+ if (users.length > 1) {
+ return Text(' and ${users.length - 1} $other', style: _buildUsernameTextStyle());
+ } else {
+ return const SizedBox.shrink();
+ }
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/login/login_screen.dart b/videocall_webrtc_sample/lib/presentation/screens/login/login_screen.dart
new file mode 100644
index 0000000..678d64c
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/login/login_screen.dart
@@ -0,0 +1,111 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/login/login_screen_view_model.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/login/widgets/error_password_text_field.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/login/widgets/header_input_text_field.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/login/widgets/login_text_field.dart';
+
+import '../../utils/notification_utils.dart';
+import '../users/users_screen.dart';
+import '../widgets/decorated_app_bar.dart';
+import 'widgets/error_login_text_field.dart';
+import 'widgets/login_button.dart';
+import 'widgets/login_heading.dart';
+import 'widgets/login_progress_indicator.dart';
+
+class LoginScreen extends StatefulWidget {
+ static show(BuildContext context) {
+ Navigator.push(context, MaterialPageRoute(builder: (_) => const LoginScreen()));
+ }
+
+ static showAndClearStack(BuildContext context) {
+ Navigator.pushAndRemoveUntil(
+ context, MaterialPageRoute(builder: (_) => const LoginScreen()), (_) => false);
+ }
+
+ const LoginScreen({super.key});
+
+ @override
+ State createState() => _LoginScreenState();
+}
+
+class _LoginScreenState extends State {
+ final TextEditingController loginTextController = TextEditingController();
+ final TextEditingController passwordTextController = TextEditingController();
+ final GlobalKey _formKey = GlobalKey();
+ final LoginScreenViewModel _viewModel = LoginScreenViewModel();
+
+ @override
+ void dispose() {
+ loginTextController.dispose();
+ passwordTextController.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ChangeNotifierProvider(
+ create: (_) => _viewModel,
+ child: Scaffold(
+ appBar: DecoratedAppBar(
+ appBar: AppBar(
+ centerTitle: true,
+ title: const Text('Enter to Video chat', style: TextStyle(
+ color: Colors.white
+ )),
+ backgroundColor: const Color(0xff3978fc))),
+ body: Form(
+ key: _formKey,
+ child: ListView(padding: const EdgeInsets.only(left: 16, right: 16), children: [
+ const LoginHeading(),
+ const HeaderInputTextField(text: "Login", marginTop: 28),
+ LoginTextField(controller: loginTextController),
+ const ErrorLoginTextField(),
+ const HeaderInputTextField(text: "Password", marginTop: 16),
+ LoginTextField(controller: passwordTextController),
+ const ErrorPasswordTextField(),
+ LoginButton(
+ callback: (buttonContext) async {
+ String login = loginTextController.text.trim();
+ String password = passwordTextController.text.trim();
+
+ bool isValidCredentials = _viewModel.isValidCredentials(login, password);
+ if (isValidCredentials) {
+ await _viewModel.login(login, password);
+ FocusScope.of(buttonContext).unfocus();
+ }
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.loading,
+ builder: (_, loading, __) {
+ if (loading) {
+ return const LoginProgressIndicator();
+ }
+ if (!loading && _viewModel.isLoggedIn) {
+ _showUsersScreen(context);
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.errorMessage,
+ builder: (_, errorMessage, __) {
+ if (errorMessage?.isNotEmpty ?? false) {
+ _showErrorSnackbar(errorMessage!, context);
+ }
+ return const SizedBox.shrink();
+ })
+ ]))));
+ }
+
+ void _showUsersScreen(BuildContext context) {
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ UsersScreen.showAndClearStack(context);
+ });
+ }
+
+ void _showErrorSnackbar(String errorMessage, BuildContext context) {
+ NotificationUtils.showSnackBarError(context, errorMessage);
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/login/login_screen_view_model.dart b/videocall_webrtc_sample/lib/presentation/screens/login/login_screen_view_model.dart
new file mode 100644
index 0000000..2fd88cc
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/login/login_screen_view_model.dart
@@ -0,0 +1,75 @@
+import 'package:flutter/services.dart';
+import 'package:quickblox_sdk/auth/module.dart';
+import 'package:videocall_webrtc_sample/dependency/dependency_impl.dart';
+import 'package:videocall_webrtc_sample/managers/auth_manager.dart';
+import 'package:videocall_webrtc_sample/managers/storage_manager.dart';
+import 'package:videocall_webrtc_sample/presentation/base_view_model.dart';
+import 'package:videocall_webrtc_sample/presentation/utils/error_parser.dart';
+
+class LoginScreenViewModel extends BaseViewModel {
+ static const MIN_LENGTH = 3;
+ static const MAX_LENGTH_LOGIN = 50;
+
+ final AuthManager _authManager = DependencyImpl.getInstance().getAuthManager();
+ final StorageManager _storageManager = DependencyImpl.getInstance().getStorageManager();
+
+ bool isLoggedIn = false;
+ bool isLoginError = false;
+ bool isPasswordError = false;
+
+ Future login(String userLogin, String userPassword) async {
+ try {
+ hideError();
+ isLoggedIn = false;
+ showLoading();
+ QBLoginResult qbLoginResult = await _authManager.login(userLogin, userPassword);
+
+ if (qbLoginResult.qbUser?.id != null) {
+ _storageManager.saveUserId(qbLoginResult.qbUser!.id!);
+ _storageManager.saveUserLogin(userLogin);
+ String? name = qbLoginResult.qbUser?.fullName ?? qbLoginResult.qbUser?.login;
+ _storageManager.saveUserName(name!);
+ _storageManager.saveUserPassword(userPassword);
+ }
+ isLoggedIn = true;
+ hideLoading();
+ } on PlatformException catch (e) {
+ hideLoading();
+ showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ bool isValidCredentials(String login, String password) {
+ isLoginError = false;
+ isPasswordError = false;
+ final isValidLogin = _validLogin(login);
+ final isValidPassword = _validPassword(password);
+
+ if (!isValidLogin) {
+ isLoginError = true;
+ }
+ if (!isValidPassword) {
+ isPasswordError = true;
+ }
+ notifyListeners();
+ return isValidLogin && isValidPassword;
+ }
+
+ bool _validLogin(String login) {
+ if (login.isEmpty || login.length < MIN_LENGTH || login.length > MAX_LENGTH_LOGIN) {
+ return false;
+ }
+ int min = MIN_LENGTH - 1;
+ int max = MAX_LENGTH_LOGIN - 1;
+ bool validLogin = RegExp('^[a-zA-Z][a-zA-Z0-9]{$min,$max}\$').hasMatch(login);
+ bool validEmail = RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(login);
+ return validLogin || validEmail;
+ }
+
+ bool _validPassword(String password) {
+ if (password.isEmpty) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/login/widgets/error_login_text_field.dart b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/error_login_text_field.dart
new file mode 100644
index 0000000..cf5adb1
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/error_login_text_field.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import '../login_screen_view_model.dart';
+
+class ErrorLoginTextField extends StatelessWidget {
+ const ErrorLoginTextField({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Selector(
+ selector: (_, viewModel) => viewModel.isLoginError,
+ builder: (_, isError, __) {
+ if (isError) {
+ return _buildErrorText();
+ }
+ return const SizedBox.shrink();
+ },
+ );
+ }
+
+ Widget _buildErrorText() {
+ return const Padding(
+ padding: EdgeInsets.only(top: 10),
+ child: Text(
+ "E-mail or login should be in a range from 3 to 50. First character must be a letter.",
+ style: TextStyle(color: Colors.red, fontSize: 12)),
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/login/widgets/error_password_text_field.dart b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/error_password_text_field.dart
new file mode 100644
index 0000000..b5a43e6
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/error_password_text_field.dart
@@ -0,0 +1,30 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import '../login_screen_view_model.dart';
+
+class ErrorPasswordTextField extends StatelessWidget {
+ const ErrorPasswordTextField({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Selector(
+ selector: (_, viewModel) => viewModel.isPasswordError,
+ builder: (_, isError, __) {
+ if (isError) {
+ return _buildErrorText();
+ }
+ return const SizedBox.shrink();
+ },
+ );
+ }
+
+ Widget _buildErrorText() {
+ return const Padding(
+ padding: EdgeInsets.only(top: 10),
+ child: Text(
+ "Password cannot be empty",
+ style: TextStyle(color: Colors.red, fontSize: 12)),
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/login/widgets/header_input_text_field.dart b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/header_input_text_field.dart
new file mode 100644
index 0000000..79149a7
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/header_input_text_field.dart
@@ -0,0 +1,23 @@
+import 'package:flutter/cupertino.dart';
+
+class HeaderInputTextField extends StatelessWidget {
+ final String text;
+ final double marginTop;
+
+ const HeaderInputTextField({
+ required this.text,
+ required this.marginTop,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Padding(
+ padding: EdgeInsets.only(top: marginTop),
+ child: Text(
+ text,
+ style: const TextStyle(color: Color(0x85333333), fontSize: 13),
+ ),
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_button.dart b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_button.dart
new file mode 100644
index 0000000..732239c
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_button.dart
@@ -0,0 +1,48 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+
+import '../login_screen_view_model.dart';
+
+class LoginButton extends StatelessWidget {
+ final void Function(BuildContext) callback;
+ const LoginButton({
+ super.key,
+ required this.callback,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Selector(
+ selector: (_, viewModel) => viewModel.loading,
+ builder: (_, isLoggingIn, __) {
+ return Container(
+ padding: const EdgeInsets.only(top: 42, left: 64, right: 64),
+ child: ElevatedButton(
+ onPressed: isLoggingIn ? null :() {
+ callback(context);
+ },
+ style: ButtonStyle(
+ elevation: MaterialStateProperty.resolveWith(
+ (states) => states.contains(MaterialState.disabled) ? null : 3),
+ shadowColor: MaterialStateProperty.resolveWith((states) =>
+ states.contains(MaterialState.disabled)
+ ? const Color(0xff99a9c6)
+ : const Color(0x403978fc)),
+ backgroundColor: MaterialStateProperty.resolveWith((states) =>
+ states.contains(MaterialState.disabled)
+ ? const Color(0xff99a9c6)
+ : const Color(0xff3978fc)),
+ ),
+ child: Container(
+ padding: const EdgeInsets.only(top: 12, bottom: 12),
+ child: const Text(
+ 'Login',
+ style: TextStyle(fontSize: 17, color: Colors.white),
+ ),
+ ),
+ ),
+ );
+ },
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_heading.dart b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_heading.dart
new file mode 100644
index 0000000..608a312
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_heading.dart
@@ -0,0 +1,17 @@
+import 'package:flutter/cupertino.dart';
+
+class LoginHeading extends StatelessWidget {
+ const LoginHeading({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.only(top: 28),
+ child: const Text(
+ 'Please enter your login \n and password',
+ textAlign: TextAlign.center,
+ style: TextStyle(fontSize: 17),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_progress_indicator.dart b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_progress_indicator.dart
new file mode 100644
index 0000000..b7190a0
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_progress_indicator.dart
@@ -0,0 +1,15 @@
+import 'package:flutter/material.dart';
+
+class LoginProgressIndicator extends StatelessWidget {
+ const LoginProgressIndicator({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return const Center(
+ child: Padding(
+ padding: EdgeInsets.symmetric(vertical: 20.0),
+ child: CircularProgressIndicator(color: Color(0xff3978fc)),
+ ),
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_text_field.dart b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_text_field.dart
new file mode 100644
index 0000000..da40a35
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/login/widgets/login_text_field.dart
@@ -0,0 +1,51 @@
+import 'package:flutter/material.dart';
+
+class LoginTextField extends StatelessWidget {
+ final TextEditingController controller;
+
+ const LoginTextField({
+ required this.controller,
+ super.key,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ padding: const EdgeInsets.only(top: 11),
+ decoration: _buildTextFieldDecoration(),
+ child: TextFormField(
+ controller: controller,
+ textInputAction: TextInputAction.next,
+ style: const TextStyle(fontSize: 17),
+ keyboardType: TextInputType.text,
+ decoration: InputDecoration(
+ errorMaxLines: 2,
+ contentPadding: const EdgeInsets.all(10),
+ filled: true,
+ fillColor: Colors.white,
+ border: _buildWhiteBorder(),
+ focusedBorder: _buildWhiteBorder(),
+ enabledBorder: _buildWhiteBorder(),
+ ),
+ ),
+ );
+ }
+
+ BoxDecoration _buildTextFieldDecoration() {
+ return const BoxDecoration(
+ boxShadow: [
+ BoxShadow(
+ color: Color.fromARGB(255, 217, 229, 255),
+ offset: Offset(0, 6),
+ blurRadius: 11.0,
+ ),
+ ],
+ );
+ }
+
+ OutlineInputBorder _buildWhiteBorder() {
+ return const OutlineInputBorder(
+ borderSide: BorderSide(color: Colors.white),
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/splash/splash_screen.dart b/videocall_webrtc_sample/lib/presentation/screens/splash/splash_screen.dart
new file mode 100644
index 0000000..f9e1db5
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/splash/splash_screen.dart
@@ -0,0 +1,79 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_svg/svg.dart';
+import 'package:provider/provider.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/login/login_screen.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/splash/splash_screen_view_model.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/splash/widgets/splash_footer.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/users/users_screen.dart';
+import 'package:videocall_webrtc_sample/presentation/utils/notification_utils.dart';
+
+import 'widgets/splash_progress_bar.dart';
+
+class SplashScreen extends StatelessWidget {
+ const SplashScreen({super.key});
+
+ void _showLoginScreen(BuildContext context) {
+ Future.delayed(Duration.zero, () => LoginScreen.showAndClearStack(context));
+ }
+
+ void _showUsersScreen(BuildContext context) {
+ Future.delayed(Duration.zero, () => UsersScreen.showAndClearStack(context));
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final SplashScreenViewModel viewModel = SplashScreenViewModel();
+ viewModel.enableLogging();
+ viewModel.initQBSDK();
+ viewModel.checkSavedUserAndLogin();
+
+ return ChangeNotifierProvider(
+ create: (_) => viewModel,
+ child: Scaffold(
+ body: Container(
+ color: const Color(0xff3978fc),
+ child: Stack(
+ fit: StackFit.expand,
+ children: [
+ Center(child: SvgPicture.asset('assets/icons/qb-logo.svg')),
+ Selector(
+ selector: (_, viewModel) => viewModel.loading,
+ builder: (_, isLoading, __) {
+ if (isLoading) {
+ return const SplashProgressBar();
+ }
+
+ if (viewModel.isLoggedIn) {
+ _showUsersScreen(context);
+ } else {
+ _showLoginScreen(context);
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ const SplashFooter(),
+ Selector(
+ selector: (_, viewModel) => viewModel.errorMessage,
+ builder: (_, errorMessage, __) {
+ if (errorMessage?.isNotEmpty == true) {
+ _showErrorSnackbar(errorMessage!, context);
+ }
+ return const SizedBox.shrink();
+ },
+ ),
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _showErrorSnackbar(String errorMessage, BuildContext context) {
+ NotificationUtils.showSnackBarError(context, errorMessage, errorCallback: () {
+ final SplashScreenViewModel viewModel =
+ Provider.of(context, listen: false);
+ viewModel.hideError();
+ viewModel.checkSavedUserAndLogin();
+ });
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/splash/splash_screen_view_model.dart b/videocall_webrtc_sample/lib/presentation/screens/splash/splash_screen_view_model.dart
new file mode 100644
index 0000000..acde83e
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/splash/splash_screen_view_model.dart
@@ -0,0 +1,70 @@
+import 'package:flutter/services.dart';
+import 'package:quickblox_sdk/auth/module.dart';
+import 'package:videocall_webrtc_sample/dependency/dependency_impl.dart';
+import 'package:videocall_webrtc_sample/main.dart';
+import 'package:videocall_webrtc_sample/managers/auth_manager.dart';
+import 'package:videocall_webrtc_sample/managers/storage_manager.dart';
+import 'package:videocall_webrtc_sample/presentation/base_view_model.dart';
+
+import '../../../managers/settings_manager.dart';
+import '../../utils/error_parser.dart';
+
+class SplashScreenViewModel extends BaseViewModel {
+ final SettingsManager _settingsManager = DependencyImpl.getInstance().getSettingsManager();
+ final StorageManager _storageManager = DependencyImpl.getInstance().getStorageManager();
+ final AuthManager _authManager = DependencyImpl.getInstance().getAuthManager();
+
+ bool isLoggedIn = false;
+
+ Future initQBSDK() async {
+ try {
+ await _settingsManager.init(APPLICATION_ID, AUTH_KEY, AUTH_SECRET, ACCOUNT_KEY);
+
+ String url = ICE_SEVER_URL;
+ if (url.isNotEmpty) {
+ String userName = ICE_SERVER_USER;
+ String password = ICE_SERVER_PASSWORD;
+ await _settingsManager.setIceServers(url, userName, password);
+ }
+ } on PlatformException catch (e) {
+ showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future enableLogging() async {
+ try {
+ await _settingsManager.enableXMPPLogging();
+ await _settingsManager.enableLogging();
+ } on PlatformException catch (e) {
+ showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future checkSavedUserAndLogin() async {
+ try {
+ showLoading();
+
+ bool isExistSavedUser = await _storageManager.isExistSavedUser();
+ if (isExistSavedUser) {
+ String userLogin = await _storageManager.getUserLogin();
+ String userPassword = await _storageManager.getUserPassword();
+ await _login(userLogin, userPassword);
+ }
+
+ hideLoading();
+ } on PlatformException catch (e) {
+ hideLoading();
+ showError(ErrorParser.parseFrom(e));
+ }
+ }
+
+ Future _login(String userLogin, String userPassword) async {
+ QBLoginResult qbLoginResult = await _authManager.login(userLogin, userPassword);
+ if (qbLoginResult.qbUser?.id != null) {
+ _storageManager.saveUserId(qbLoginResult.qbUser!.id!);
+ isLoggedIn = true;
+ } else {
+ isLoggedIn = false;
+ }
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/splash/widgets/splash_footer.dart b/videocall_webrtc_sample/lib/presentation/screens/splash/widgets/splash_footer.dart
new file mode 100644
index 0000000..1c2e946
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/splash/widgets/splash_footer.dart
@@ -0,0 +1,19 @@
+import 'package:flutter/material.dart';
+
+class SplashFooter extends StatelessWidget {
+ const SplashFooter({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ alignment: Alignment.bottomCenter,
+ child: const Padding(
+ padding: EdgeInsets.only(bottom: 40),
+ child: Text(
+ 'Flutter Video Call Sample',
+ style: TextStyle(fontSize: 18, color: Colors.white),
+ ),
+ ),
+ );
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/splash/widgets/splash_progress_bar.dart b/videocall_webrtc_sample/lib/presentation/screens/splash/widgets/splash_progress_bar.dart
new file mode 100644
index 0000000..d2babe8
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/splash/widgets/splash_progress_bar.dart
@@ -0,0 +1,16 @@
+import 'package:flutter/material.dart';
+
+class SplashProgressBar extends StatelessWidget{
+ const SplashProgressBar({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return Container(
+ alignment: Alignment.bottomCenter,
+ child: const Padding(
+ padding: EdgeInsets.only(bottom: 150),
+ child: CircularProgressIndicator(color: Colors.white, strokeWidth: 4.0),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/videocall_webrtc_sample/lib/presentation/screens/users/callback/app_bar_callback.dart b/videocall_webrtc_sample/lib/presentation/screens/users/callback/app_bar_callback.dart
new file mode 100644
index 0000000..feeadc7
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/users/callback/app_bar_callback.dart
@@ -0,0 +1,9 @@
+abstract class AppBarCallback {
+ void onLogIn();
+
+ void onLogOut();
+
+ void onVideoCall();
+
+ void onAudioCall();
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/users/callback/app_bar_callback_impl.dart b/videocall_webrtc_sample/lib/presentation/screens/users/callback/app_bar_callback_impl.dart
new file mode 100644
index 0000000..e84badd
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/users/callback/app_bar_callback_impl.dart
@@ -0,0 +1,30 @@
+import 'app_bar_callback.dart';
+
+class AppBarCallbackImpl implements AppBarCallback {
+ AppBarCallbackImpl({
+ required void Function() onLogIn,
+ required void Function() onLogOut,
+ required void Function() onVideoCall,
+ required void Function() onAudioCall,
+ }) : _onLogIn = onLogIn,
+ _onLogOut = onLogOut,
+ _onVideoCall = onVideoCall,
+ _onAudioCall = onAudioCall;
+
+ final void Function() _onLogIn;
+ final void Function() _onLogOut;
+ final void Function() _onVideoCall;
+ final void Function() _onAudioCall;
+
+ @override
+ void onLogIn() => _onLogIn();
+
+ @override
+ void onLogOut() => _onLogOut();
+
+ @override
+ void onVideoCall() => _onVideoCall();
+
+ @override
+ void onAudioCall() => _onAudioCall();
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/users/users_screen.dart b/videocall_webrtc_sample/lib/presentation/screens/users/users_screen.dart
new file mode 100644
index 0000000..207d608
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/users/users_screen.dart
@@ -0,0 +1,285 @@
+import 'package:flutter/material.dart';
+import 'package:provider/provider.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:videocall_webrtc_sample/entities/user_entity.dart';
+import 'package:videocall_webrtc_sample/presentation/dialogs/yes_no_dialog.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/audio/audio_call/audio_call_screen.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/audio/incoming/incoming_audio_call_screen.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/call/video/video_call/video_call_screen.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/login/login_screen.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/users/callback/app_bar_callback.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/users/callback/app_bar_callback_impl.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/users/users_screen_view_model.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/users/widgets/users_app_bar.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/users/widgets/users_list_item.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/users/widgets/users_list_loading_item.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/users/widgets/users_search_bar.dart';
+import 'package:videocall_webrtc_sample/presentation/screens/widgets/decorated_app_bar.dart';
+import 'package:videocall_webrtc_sample/presentation/utils/debouncer.dart';
+
+import '../../utils/notification_utils.dart';
+import '../call/video/incoming/incoming_video_call_screen.dart';
+
+class UsersScreen extends StatefulWidget {
+ static show(BuildContext context) {
+ Navigator.push(context, MaterialPageRoute(builder: (_) => const UsersScreen()));
+ }
+
+ static showAndClearStack(BuildContext context) {
+ Navigator.pushAndRemoveUntil(
+ context, MaterialPageRoute(builder: (_) => const UsersScreen()), (_) => false);
+ }
+
+ const UsersScreen({super.key});
+
+ @override
+ State createState() => _UsersScreenState();
+}
+
+class _UsersScreenState extends State {
+ final UsersScreenViewModel _viewModel = UsersScreenViewModel();
+ final ScrollController _scrollController = ScrollController();
+ final TextEditingController _searchController = TextEditingController();
+
+ final _searchDebouncer = Debouncer();
+ bool _search = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _viewModel.init();
+ _scrollController.addListener(_scrollListener);
+ }
+
+ void _scrollListener() {
+ double? maxScroll = _scrollController.position.maxScrollExtent;
+ double? currentScroll = _scrollController.position.pixels;
+ if (maxScroll == currentScroll) {
+ if (_search) {
+ _viewModel.searchNextUsers = true;
+ _viewModel.searchUsers(_searchController.text.trim());
+ } else {
+ _viewModel.loadNextUsers = true;
+ _viewModel.loadUsers();
+ }
+ }
+ }
+
+ @override
+ void dispose() {
+ _scrollController.dispose();
+ _searchController.dispose();
+ _searchDebouncer.dispose();
+ super.dispose();
+ }
+
+ AppBarCallback createAppBarCallback() {
+ return AppBarCallbackImpl(onLogIn: () {
+ _showLoginScreen();
+ }, onLogOut: () {
+ _showLogoutDialog();
+ }, onAudioCall: () async {
+ List selectedUsers = _viewModel.getSelectedQbUsers();
+
+ if (selectedUsers.isEmpty) {
+ _showErrorSnackbar("To make a call you need to select at least one opponent.", context);
+ return;
+ }
+
+ await _viewModel.checkAudioPermissions();
+ await _viewModel.startAudioCall();
+ setState(() {
+ _viewModel.clearSelectedUsers();
+ });
+ _showAudioCallScreen(selectedUsers);
+ }, onVideoCall: () async {
+ List selectedUsers = _viewModel.getSelectedQbUsers();
+
+ if (selectedUsers.isEmpty) {
+ _showErrorSnackbar("To make a call you need to select at least one opponent.", context);
+ return;
+ }
+
+ await _viewModel.checkVideoPermissions();
+
+ List users = await _viewModel.addCurrentUserToList(selectedUsers);
+ await _viewModel.addVideoCallEntities(users);
+ setState(() {
+ _viewModel.clearSelectedUsers();
+ });
+ _showVideoCallScreen(selectedUsers);
+ });
+ }
+
+ Future _refresh() async {
+ setState(() {
+ _search = false;
+ _viewModel.loadNextUsers = false;
+ _viewModel.loadUsers();
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return ChangeNotifierProvider(
+ create: (context) => _viewModel,
+ child: Scaffold(
+ appBar: DecoratedAppBar(appBar: UsersAppBar(callback: createAppBarCallback())),
+ body: Column(
+ children: [
+ UsersSearchBar(
+ searchController: _searchController,
+ callback: (text) {
+ if (text.length >= 3) {
+ _search = true;
+ _viewModel.searchNextUsers = false;
+ _searchDebouncer.call(() => _viewModel.searchUsers(text));
+ }
+ if (text.isEmpty) {
+ _search = false;
+ _viewModel.loadNextUsers = false;
+ _viewModel.loadUsers();
+ }
+ },
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.usersLoading,
+ builder: (_, isLoading, __) {
+ return isLoading
+ ? const Center(
+ child: CircularProgressIndicator(),
+ )
+ : const SizedBox.shrink();
+ },
+ ),
+ Expanded(
+ child: Selector>(
+ selector: (_, viewModel) => viewModel.loadedUsersSet.toList(),
+ builder: (_, qbUsers, __) {
+ return qbUsers.isEmpty
+ ? const SizedBox.shrink()
+ : RefreshIndicator(
+ onRefresh: _refresh,
+ child: ListView.builder(
+ addAutomaticKeepAlives: true,
+ controller: _scrollController,
+ itemCount: qbUsers.length,
+ itemBuilder: (_, index) {
+ if (index == qbUsers.length - 1) {
+ return Column(
+ children: [
+ UsersListItem(
+ qbUsers[index], _viewModel.handleChangedSelectedUsers),
+ const UsersListLoadingItem(),
+ ],
+ );
+ }
+ return UsersListItem(
+ qbUsers[index], _viewModel.handleChangedSelectedUsers);
+ },
+ ));
+ },
+ ),
+ ),
+ Selector(
+ selector: (_, viewModel) => viewModel.errorMessage,
+ builder: (_, errorMessage, __) {
+ if (errorMessage?.isNotEmpty ?? false) {
+ _showErrorSnackbar(errorMessage!, context);
+ }
+ return const SizedBox.shrink();
+ }),
+ Selector(
+ selector: (_, viewModel) => viewModel.receivedCall,
+ builder: (_, isGettingCall, __) {
+ if (isGettingCall) {
+ if (!_viewModel.isVideoCall) {
+ _viewModel.checkAudioPermissions().then(
+ (value) => {if (value) _loadOpponentsAndShowIncomingAudioCallScreen()});
+ } else {
+ _viewModel
+ .checkVideoPermissions()
+ .then((value) => _loadOpponentsAndShowIncomingVideoCallScreen());
+ }
+ }
+ return const SizedBox.shrink();
+ }),
+ ],
+ )),
+ );
+ }
+
+ void _showLogoutDialog() {
+ return YesNoDialog(
+ onPressedPrimary: () async {
+ Navigator.pop(context);
+ await _viewModel.logout();
+ },
+ onPressedSecondary: () => Navigator.pop(context),
+ primaryButtonText: 'Logout',
+ secondaryButtonText: 'Cancel')
+ .show(context, title: 'Press Logout to continue', dismissTouchOutside: true);
+ }
+
+ void _showErrorSnackbar(String errorMessage, BuildContext context) {
+ return NotificationUtils.showSnackBarError(context, errorMessage);
+ }
+
+ void _showLoginScreen() {
+ NotificationUtils.hideSnackBar(context);
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ LoginScreen.showAndClearStack(context);
+ });
+ }
+
+ void _showAudioCallScreen(List users) {
+ NotificationUtils.hideSnackBar(context);
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => AudioCallScreen(
+ isIncoming: false,
+ opponents: users,
+ ))).then((result) {
+ _viewModel.receivedCall = false;
+ });
+ });
+ }
+
+ void _showVideoCallScreen(List users) {
+ NotificationUtils.hideSnackBar(context);
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) => VideoCallScreen(
+ isIncoming: false, callUsers: users))).then((result) {
+ _viewModel.receivedCall = false;
+ });
+ });
+ }
+
+ void _loadOpponentsAndShowIncomingAudioCallScreen() async {
+ NotificationUtils.hideSnackBar(context);
+ List? opponents = await _viewModel.loadOpponents();
+ if (opponents == null) {
+ return;
+ }
+ IncomingAudioCallScreen.show(context, opponents: opponents).then((result) {
+ _viewModel.setDefaultStateForGettingCall();
+ });
+ }
+
+ void _loadOpponentsAndShowIncomingVideoCallScreen() async {
+ NotificationUtils.hideSnackBar(context);
+ List? users = await _viewModel.loadCallUsers();
+ if (users == null) {
+ return;
+ }
+ await _viewModel.addVideoCallEntities(users);
+ IncomingVideoCallScreen.show(context, users).then((result) {
+ _viewModel.setDefaultStateForGettingCall();
+ });
+ }
+}
diff --git a/videocall_webrtc_sample/lib/presentation/screens/users/users_screen_view_model.dart b/videocall_webrtc_sample/lib/presentation/screens/users/users_screen_view_model.dart
new file mode 100644
index 0000000..c3004b2
--- /dev/null
+++ b/videocall_webrtc_sample/lib/presentation/screens/users/users_screen_view_model.dart
@@ -0,0 +1,427 @@
+import 'dart:collection';
+import 'dart:io';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:quickblox_sdk/models/qb_filter.dart';
+import 'package:quickblox_sdk/models/qb_sort.dart';
+import 'package:quickblox_sdk/models/qb_user.dart';
+import 'package:quickblox_sdk/users/constants.dart';
+import 'package:quickblox_sdk/webrtc/constants.dart';
+import 'package:videocall_webrtc_sample/dependency/dependency_impl.dart';
+import 'package:videocall_webrtc_sample/managers/auth_manager.dart';
+import 'package:videocall_webrtc_sample/managers/call_manager.dart';
+import 'package:videocall_webrtc_sample/managers/chat_manager.dart';
+import 'package:videocall_webrtc_sample/managers/storage_manager.dart';
+import 'package:videocall_webrtc_sample/managers/users_manager.dart';
+import 'package:videocall_webrtc_sample/presentation/utils/error_parser.dart';
+
+import '../../../entities/user_entity.dart';
+import '../../../managers/callback/call_subscription.dart';
+import '../../../managers/callback/call_subscription_impl.dart';
+import '../../../managers/permission_manager.dart';
+import '../../base_view_model.dart';
+
+class UsersScreenViewModel extends BaseViewModel {
+ static const int PAGE_SIZE = 100;
+
+ final AuthManager _authManager = DependencyImpl.getInstance().getAuthManager();
+ final ChatManager _chatManager = DependencyImpl.getInstance().getChatManager();
+ final StorageManager _storageManager = DependencyImpl.getInstance().getStorageManager();
+ final UsersManager _usersManager = DependencyImpl.getInstance().getUsersManager();
+ final CallManager _callManager = DependencyImpl.getInstance().getCallManager();
+ final PermissionManager _permissionManager = DependencyImpl.getInstance().getPermissionManager();
+
+ final LinkedHashSet loadedUsersSet = LinkedHashSet();
+ final HashSet selectedUsersSet = HashSet();
+
+ bool isLoggedIn = true;
+ bool isLoggingOut = false;
+ int _currentPage = 1;
+ int _currentSearchPage = 1;
+ bool usersLoading = false;
+
+ bool loadNextUsers = false;
+ bool searchNextUsers = false;
+
+ bool isVideoCall = false;
+
+ bool receivedCall = false;
+ CallSubscription? _callSubscription;
+
+ List getSelectedQbUsers() {
+ List