├── .gitignore ├── .metadata ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── README.md ├── analysis_options.yaml ├── android ├── .gitignore ├── app │ ├── build.gradle │ └── src │ │ ├── debug │ │ └── AndroidManifest.xml │ │ ├── main │ │ ├── AndroidManifest.xml │ │ ├── java │ │ │ └── io │ │ │ │ └── flutter │ │ │ │ └── plugins │ │ │ │ └── GeneratedPluginRegistrant.java │ │ ├── kotlin │ │ │ └── com │ │ │ │ └── amazonaws │ │ │ │ └── services │ │ │ │ └── chime │ │ │ │ └── flutterdemo │ │ │ │ ├── AudioVideoObserver.kt │ │ │ │ ├── FlutterVideoTileFactory.kt │ │ │ │ ├── MainActivity.kt │ │ │ │ ├── MeetingSession.kt │ │ │ │ ├── MethodCallEnum.kt │ │ │ │ ├── MethodChannelCoordinator.kt │ │ │ │ ├── MethodChannelResult.kt │ │ │ │ ├── PermissionManager.kt │ │ │ │ ├── RealtimeObserver.kt │ │ │ │ ├── ResponseEnum.kt │ │ │ │ ├── VideoTileObserver.kt │ │ │ │ └── VideoTileView.kt │ │ └── res │ │ │ ├── drawable-v21 │ │ │ └── launch_background.xml │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── values-night │ │ │ └── styles.xml │ │ │ └── values │ │ │ └── styles.xml │ │ └── profile │ │ └── AndroidManifest.xml ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── local.properties └── settings.gradle ├── ios ├── .gitignore ├── Flutter │ ├── AppFrameworkInfo.plist │ ├── Debug.xcconfig │ └── Release.xcconfig ├── Podfile ├── Podfile.lock ├── Runner.xcodeproj │ ├── project.pbxproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── IDEWorkspaceChecks.plist │ │ └── WorkspaceSettings.xcsettings └── Runner │ ├── AppDelegate.swift │ ├── Assets.xcassets │ ├── AppIcon.appiconset │ │ ├── Contents.json │ │ ├── Icon-App-1024x1024@1x.png │ │ ├── Icon-App-20x20@1x.png │ │ ├── Icon-App-20x20@2x.png │ │ ├── Icon-App-20x20@3x.png │ │ ├── Icon-App-29x29@1x.png │ │ ├── Icon-App-29x29@2x.png │ │ ├── Icon-App-29x29@3x.png │ │ ├── Icon-App-40x40@1x.png │ │ ├── Icon-App-40x40@2x.png │ │ ├── Icon-App-40x40@3x.png │ │ ├── Icon-App-60x60@2x.png │ │ ├── Icon-App-60x60@3x.png │ │ ├── Icon-App-76x76@1x.png │ │ ├── Icon-App-76x76@2x.png │ │ └── Icon-App-83.5x83.5@2x.png │ └── LaunchImage.imageset │ │ ├── Contents.json │ │ ├── LaunchImage.png │ │ ├── LaunchImage@2x.png │ │ ├── LaunchImage@3x.png │ │ └── README.md │ ├── AudioVideoObserver.swift │ ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard │ ├── FlutterVideoTileFactory.swift │ ├── Info.plist │ ├── MeetingSession.swift │ ├── MethodCallEnum.swift │ ├── MethodChannelCoordinator.swift │ ├── MethodChannelResponse.swift │ ├── RealtimeObserver.swift │ ├── ResponseEnums.swift │ ├── Runner-Bridging-Header.h │ ├── VideoTileObserver.swift │ └── VideoTileView.swift ├── lib ├── api.dart ├── api_config.dart ├── attendee.dart ├── interfaces │ ├── audio_devices_interface.dart │ ├── audio_video_interface.dart │ ├── realtime_interface.dart │ └── video_tile_interface.dart ├── logger.dart ├── main.dart ├── method_channel_coordinator.dart ├── response_enums.dart ├── video_tile.dart ├── view_models │ ├── join_meeting_view_model.dart │ └── meeting_view_model.dart └── views │ ├── join_meeting.dart │ ├── meeting.dart │ ├── screenshare.dart │ └── style.dart ├── pubspec.lock ├── pubspec.yaml └── test └── widget_test.dart /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | 36 | # Web related 37 | lib/generated_plugin_registrant.dart 38 | 39 | # Symbolication related 40 | app.*.symbols 41 | 42 | # Obfuscation related 43 | app.*.map.json 44 | 45 | # Android Studio will place build artifacts here 46 | /android/app/debug 47 | /android/app/profile 48 | /android/app/release 49 | 50 | *.xcframework 51 | *.framework 52 | /lib/video_tile_widget_test.dart 53 | android/local.properties -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 85684f9300908116a78138ea4c6036c35c9a1236 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 17 | base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 18 | - platform: android 19 | create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 20 | base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 21 | - platform: ios 22 | create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 23 | base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 24 | - platform: linux 25 | create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 26 | base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 27 | - platform: macos 28 | create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 29 | base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 30 | - platform: web 31 | create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 32 | base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 33 | - platform: windows 34 | create_revision: 85684f9300908116a78138ea4c6036c35c9a1236 35 | base_revision: 85684f9300908116a78138ea4c6036c35c9a1236 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *main* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this 4 | software and associated documentation files (the "Software"), to deal in the Software 5 | without restriction, including without limitation the rights to use, copy, modify, 6 | merge, publish, distribute, sublicense, and/or sell copies of the Software, and to 7 | permit persons to whom the Software is furnished to do so. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, 10 | INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 11 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 12 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 13 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 14 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Amazon Chime SDK Flutter Demo 2 | The Amazon Chime SDK is a set of real-time communications components that developers can use to quickly add audio calling, video calling, and screen sharing capabilities to their own applications. Developers can leverage the same communication infrastructure and services that power Amazon Chime, an online meetings service from AWS, to deliver engaging experiences in their applications. For instance, they can add video calling to a healthcare application so patients can consult remotely with doctors on health issues, or add audio calling to a company website so customers can quickly connect with sales. By using the Amazon Chime SDK, developers can eliminate the cost, complexity, and friction of creating and maintaining their own real-time communication infrastructure and services. 3 | ​ 4 | This demo shows how to integrate the [Amazon Chime SDK](https://aws.amazon.com/blogs/business-productivity/amazon-chime-sdks-ios-android/) into your Flutter application. 5 | ​ 6 | For more details about the SDK APIs, please refer to the **Getting Started** guide of the following SDK repositories: 7 | * [amazon-chime-sdk-android](https://github.com/aws/amazon-chime-sdk-android/blob/master/guides/01_Getting_Started.md) 8 | * [amazon-chime-sdk-ios](https://github.com/aws/amazon-chime-sdk-ios/blob/master/guides/01_Getting_Started.md) 9 | ​ 10 | > *Note: Deploying the Amazon Chime SDK demo applications contained in this repository will cause your AWS Account to be billed for services, including the Amazon Chime SDK, used by the application.* 11 | --- 12 | ​ 13 | # How to Run the Flutter Demo Application​ 14 | 15 | ## Prerequisites 16 | The demo application is able run on both iOS and Android. For managing Amazon Chime SDK as dependency, CocoaPods is utilized on iOS, Maven Central repository with Gradle is utilized on Android. In order to run the demo, make sure the following are installed/prepared: 17 | 18 | For both iOS and Android: 19 | - Install [Flutter SDK](https://docs.flutter.dev/get-started/install) 20 | 21 | For running on iOS: 22 | - MacOS is needed 23 | - Install [XCode](https://apps.apple.com/us/app/xcode/id497799835?mt=12) 24 | - Install [CocoaPods](https://guides.cocoapods.org/using/getting-started.html#getting-started) 25 | - If running on simulator, follow this [link](https://developer.apple.com/documentation/xcode/installing-additional-simulator-runtimes) to create iOS Simulators 26 | - If running on physical device, Apple Developer [account](https://developer.apple.com/) is needed. 27 | 28 | For running on Android: 29 | - Install [Android Studio](https://developer.android.com/studio/install) 30 | - Install [Gradle](https://gradle.org/install/) 31 | - Android physical device is needed (*currently x86 architecture/simulators are not supported*) 32 | 33 | > *Note: The demo application is not necessarily running with the latest Amazon Chime SDK, the current SDK version can be found [here](https://github.com/aws-samples/amazon-chime-sdk-flutter-demo/blob/main/ios/Podfile#L8) for iOS and [here](https://github.com/aws-samples/amazon-chime-sdk-flutter-demo/blob/main/android/app/build.gradle#L76-L77) for Android.* 34 | 35 | ## 1. Clone the repository 36 | Run `git clone` to download the source code 37 | 38 | ## 2. Deploy the serverless demo 39 | Follow the instructions in [amazon-chime-sdk-js](https://github.com/aws/amazon-chime-sdk-js/tree/master/demos/serverless) to deploy the serverless demo. 40 | > *Note: The Flutter demo doesn’t require authentication since the serverless demo does not provide the functionality, builders need to implement authentication for their own backend service.* 41 | 42 | ## 3. Update the server URLs 43 | Update `apiUrl` and `region` in `lib/api_config.dart` with the server URL and region of the serverless demo you created. `apiUrl` format: `https://.execute-api..amazonaws.com/Prod/`. 44 | 45 | ## 4. Build and run 46 | 47 | ### Android 48 | * Connect a physical Android testing device (*we currently do not support x86 architecture/simulators*) to your computer 49 | * Run `flutter run` under the root directory to start the demo app on the device 50 | 51 | ### iOS 52 | * Connect a physical iOS testing device or start iOS simulator 53 | * Run `pod install` under `./ios/` directory to install Chime SDK dependencies 54 | * Run `flutter run` under the root directory to start the demo app on the device/simulator 55 | 56 | ## 5. Cleanup 57 | If you no longer want to keep the demo active in your AWS account and want to avoid incurring AWS charges, the demo resources can be removed. Delete the two AWS CloudFormation (https://aws.amazon.com/cloudformation/) stacks created in the prerequisites that can be found in the AWS CloudFormation console (https://console.aws.amazon.com/cloudformation/home). 58 | 59 | ​ 60 | **Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.** 61 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | def localProperties = new Properties() 7 | def localPropertiesFile = rootProject.file('local.properties') 8 | if (localPropertiesFile.exists()) { 9 | localPropertiesFile.withReader('UTF-8') { reader -> 10 | localProperties.load(reader) 11 | } 12 | } 13 | 14 | def flutterRoot = localProperties.getProperty('flutter.sdk') 15 | if (flutterRoot == null) { 16 | throw new Exception("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 17 | } 18 | 19 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 20 | if (flutterVersionCode == null) { 21 | flutterVersionCode = '1' 22 | } 23 | 24 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 25 | if (flutterVersionName == null) { 26 | flutterVersionName = '1.0' 27 | } 28 | 29 | apply plugin: 'com.android.application' 30 | apply plugin: 'kotlin-android' 31 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 32 | 33 | android { 34 | compileSdkVersion flutter.compileSdkVersion 35 | ndkVersion flutter.ndkVersion 36 | 37 | compileOptions { 38 | sourceCompatibility JavaVersion.VERSION_1_8 39 | targetCompatibility JavaVersion.VERSION_1_8 40 | } 41 | 42 | kotlinOptions { 43 | jvmTarget = '1.8' 44 | } 45 | 46 | sourceSets { 47 | main.java.srcDirs += 'src/main/kotlin' 48 | } 49 | 50 | defaultConfig { 51 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 52 | applicationId "com.amazonaws.services.chime.flutterdemo" 53 | // You can update the following values to match your application needs. 54 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 55 | minSdkVersion 21 56 | targetSdkVersion flutter.targetSdkVersion 57 | versionCode flutterVersionCode.toInteger() 58 | versionName flutterVersionName 59 | } 60 | 61 | buildTypes { 62 | release { 63 | // TODO: Add your own signing config for the release build. 64 | // Signing with the debug keys for now, so `flutter run --release` works. 65 | signingConfig signingConfigs.debug 66 | } 67 | } 68 | } 69 | 70 | flutter { 71 | source '../..' 72 | } 73 | 74 | dependencies { 75 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 76 | implementation 'software.aws.chimesdk:amazon-chime-sdk-media:0.17.2' 77 | implementation 'software.aws.chimesdk:amazon-chime-sdk:0.17.2' 78 | implementation 'androidx.appcompat:appcompat:1.3.0' 79 | } 80 | -------------------------------------------------------------------------------- /android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 12 | 20 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java: -------------------------------------------------------------------------------- 1 | package io.flutter.plugins; 2 | 3 | import androidx.annotation.Keep; 4 | import androidx.annotation.NonNull; 5 | import io.flutter.Log; 6 | 7 | import io.flutter.embedding.engine.FlutterEngine; 8 | 9 | /** 10 | * Generated file. Do not edit. 11 | * This file is generated by the Flutter tool based on the 12 | * plugins that support the Android platform. 13 | */ 14 | @Keep 15 | public final class GeneratedPluginRegistrant { 16 | private static final String TAG = "GeneratedPluginRegistrant"; 17 | public static void registerWith(@NonNull FlutterEngine flutterEngine) { 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/AudioVideoObserver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoObserver 9 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.RemoteVideoSource 10 | import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionStatus 11 | 12 | class AudioVideoObserver(val methodChannel: MethodChannelCoordinator) : AudioVideoObserver { 13 | override fun onAudioSessionCancelledReconnect() { 14 | // Out of Scope 15 | } 16 | 17 | override fun onAudioSessionDropped() { 18 | // Out of Scope 19 | } 20 | 21 | override fun onAudioSessionStarted(reconnecting: Boolean) { 22 | // Out of Scope 23 | } 24 | 25 | override fun onAudioSessionStartedConnecting(reconnecting: Boolean) { 26 | // Out of Scope 27 | } 28 | 29 | override fun onAudioSessionStopped(sessionStatus: MeetingSessionStatus) { 30 | methodChannel.callFlutterMethod(MethodCall.audioSessionDidStop, null) 31 | } 32 | 33 | override fun onConnectionBecamePoor() { 34 | // Out of Scope 35 | } 36 | 37 | override fun onConnectionRecovered() { 38 | // Out of Scope 39 | } 40 | 41 | override fun onRemoteVideoSourceAvailable(sources: List) { 42 | // Out of Scope 43 | } 44 | 45 | override fun onRemoteVideoSourceUnavailable(sources: List) { 46 | // Out of Scope 47 | } 48 | 49 | override fun onVideoSessionStarted(sessionStatus: MeetingSessionStatus) { 50 | // Out of Scope 51 | } 52 | 53 | override fun onVideoSessionStartedConnecting() { 54 | // Out of Scope 55 | } 56 | 57 | override fun onVideoSessionStopped(sessionStatus: MeetingSessionStatus) { 58 | // Out of Scope 59 | } 60 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/FlutterVideoTileFactory.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | import android.content.Context 9 | import io.flutter.plugin.common.StandardMessageCodec 10 | import io.flutter.plugin.platform.PlatformView 11 | import io.flutter.plugin.platform.PlatformViewFactory 12 | 13 | class NativeViewFactory : PlatformViewFactory(StandardMessageCodec.INSTANCE) { 14 | override fun create(context: Context?, viewId: Int, args: Any?): PlatformView { 15 | val creationParams = args as Int? 16 | return VideoTileView(context, creationParams) 17 | } 18 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | import androidx.annotation.NonNull 9 | import io.flutter.embedding.android.FlutterActivity 10 | import io.flutter.embedding.engine.FlutterEngine 11 | 12 | class MainActivity : FlutterActivity() { 13 | var methodChannel: MethodChannelCoordinator? = null 14 | 15 | override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { 16 | super.configureFlutterEngine(flutterEngine) 17 | 18 | methodChannel = 19 | MethodChannelCoordinator( 20 | flutterEngine.dartExecutor.binaryMessenger, 21 | getActivity() 22 | ) 23 | methodChannel?.setupMethodChannel() 24 | 25 | flutterEngine 26 | .platformViewsController 27 | .registry 28 | .registerViewFactory("videoTile", NativeViewFactory()) 29 | } 30 | 31 | override fun onRequestPermissionsResult( 32 | requestCode: Int, 33 | permissionsList: Array, 34 | grantResults: IntArray 35 | ) { 36 | val permissionsManager = methodChannel?.permissionsManager ?: return 37 | when (requestCode) { 38 | permissionsManager.AUDIO_PERMISSION_REQUEST_CODE -> { 39 | methodChannel?.permissionsManager?.audioCallbackReceived() 40 | } 41 | permissionsManager.VIDEO_PERMISSION_REQUEST_CODE -> { 42 | methodChannel?.permissionsManager?.videoCallbackReceived() 43 | } 44 | } 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/MeetingSession.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | import com.amazonaws.services.chime.sdk.meetings.session.DefaultMeetingSession 9 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoObserver 10 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.AudioVideoFacade 11 | import com.amazonaws.services.chime.sdk.meetings.utils.logger.ConsoleLogger 12 | 13 | object MeetingSessionManager { 14 | private val meetingSessionlogger: ConsoleLogger = ConsoleLogger() 15 | 16 | var realtimeObserver: RealtimeObserver? = null 17 | var videoTileObserver: VideoTileObserver? = null 18 | var audioVideoObserver: AudioVideoObserver? = null 19 | 20 | var meetingSession: DefaultMeetingSession? = null 21 | 22 | 23 | private val NULL_MEETING_SESSION_RESPONSE: MethodChannelResult = 24 | MethodChannelResult(false, Response.meeting_session_is_null.msg) 25 | 26 | fun startMeeting( 27 | realtimeObserver: RealtimeObserver? = null, 28 | videoTileObserver: VideoTileObserver? = null, 29 | audioVideoObserver: AudioVideoObserver? = null 30 | ): MethodChannelResult { 31 | val audioVideo: AudioVideoFacade = 32 | meetingSession?.audioVideo ?: return NULL_MEETING_SESSION_RESPONSE 33 | addObservers(realtimeObserver, videoTileObserver, audioVideoObserver) 34 | audioVideo.start() 35 | audioVideo.startRemoteVideo() 36 | return MethodChannelResult(true, Response.create_meeting_success.msg) 37 | } 38 | 39 | fun stop(): MethodChannelResult { 40 | meetingSession?.audioVideo?.stopRemoteVideo() ?: return NULL_MEETING_SESSION_RESPONSE 41 | meetingSession?.audioVideo?.stop() ?: return NULL_MEETING_SESSION_RESPONSE 42 | removeObservers() 43 | meetingSession = null 44 | return MethodChannelResult(true, Response.meeting_stopped_successfully.msg) 45 | } 46 | 47 | private fun addObservers( 48 | realtimeObserver: RealtimeObserver?, 49 | videoTileObserver: VideoTileObserver?, 50 | audioVideoObserver: AudioVideoObserver? 51 | ) { 52 | val audioVideo: AudioVideoFacade = meetingSession?.audioVideo ?: return 53 | realtimeObserver?.let { 54 | audioVideo.addRealtimeObserver(it) 55 | this.realtimeObserver = realtimeObserver 56 | meetingSessionlogger.debug("RealtimeObserver", "RealtimeObserver initialized") 57 | } 58 | audioVideoObserver?.let { 59 | audioVideo.addAudioVideoObserver(it) 60 | this.audioVideoObserver = audioVideoObserver 61 | meetingSessionlogger.debug("AudioVideoObserver", "AudioVideoObserver initialized") 62 | } 63 | videoTileObserver?.let { 64 | audioVideo.addVideoTileObserver(videoTileObserver) 65 | this.videoTileObserver = videoTileObserver 66 | meetingSessionlogger.debug("VideoTileObserver", "VideoTileObserver initialized") 67 | } 68 | } 69 | 70 | private fun removeObservers() { 71 | realtimeObserver?.let { 72 | meetingSession?.audioVideo?.removeRealtimeObserver(it) 73 | } 74 | audioVideoObserver?.let { 75 | meetingSession?.audioVideo?.removeAudioVideoObserver(it) 76 | } 77 | videoTileObserver?.let { 78 | meetingSession?.audioVideo?.removeVideoTileObserver(it) 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/MethodCallEnum.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | enum class MethodCall(val call: String) { 9 | manageAudioPermissions("manageAudioPermissions"), 10 | manageVideoPermissions("manageVideoPermissions"), 11 | initialAudioSelection("initialAudioSelection"), 12 | join("join"), 13 | stop("stop"), 14 | leave("leave"), 15 | drop("drop"), 16 | mute("mute"), 17 | unmute("unmute"), 18 | startLocalVideo("startLocalVideo"), 19 | stopLocalVideo("stopLocalVideo"), 20 | videoTileAdd("videoTileAdd"), 21 | videoTileRemove("videoTileRemove"), 22 | listAudioDevices("listAudioDevices"), 23 | updateAudioDevice("updateAudioDevice"), 24 | audioSessionDidStop("audioSessionDidStop") 25 | } 26 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/MethodChannelCoordinator.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | import com.amazonaws.services.chime.sdk.meetings.device.MediaDevice 9 | import com.amazonaws.services.chime.sdk.meetings.session.DefaultMeetingSession 10 | import com.amazonaws.services.chime.sdk.meetings.session.MediaPlacement 11 | import com.amazonaws.services.chime.sdk.meetings.session.MeetingSessionConfiguration 12 | import com.amazonaws.services.chime.sdk.meetings.session.CreateMeetingResponse 13 | import com.amazonaws.services.chime.sdk.meetings.session.Meeting 14 | import com.amazonaws.services.chime.sdk.meetings.session.CreateAttendeeResponse 15 | import com.amazonaws.services.chime.sdk.meetings.session.Attendee 16 | import com.amazonaws.services.chime.sdk.meetings.utils.logger.ConsoleLogger 17 | import com.amazonaws.services.chime.flutterdemo.MethodCall as MethodCallFlutter 18 | import android.app.Activity 19 | import android.content.Context 20 | import io.flutter.plugin.common.BinaryMessenger 21 | import io.flutter.plugin.common.MethodCall 22 | import androidx.appcompat.app.AppCompatActivity 23 | import io.flutter.plugin.common.MethodChannel 24 | 25 | class MethodChannelCoordinator(binaryMessenger: BinaryMessenger, activity: Activity) : 26 | AppCompatActivity() { 27 | val methodChannel: MethodChannel 28 | val context: Context 29 | var permissionsManager: PermissionManager = PermissionManager(activity) 30 | 31 | init { 32 | methodChannel = 33 | MethodChannel(binaryMessenger, "com.amazonaws.services.chime.flutterDemo.methodChannel") 34 | context = activity.applicationContext 35 | } 36 | 37 | private val NULL_MEETING_SESSION_RESPONSE: MethodChannelResult = 38 | MethodChannelResult(false, Response.meeting_session_is_null.msg) 39 | 40 | fun setupMethodChannel() { 41 | methodChannel.setMethodCallHandler { call, result -> 42 | val callResult: MethodChannelResult 43 | when (call.method) { 44 | MethodCallFlutter.manageAudioPermissions.call -> { 45 | permissionsManager.manageAudioPermissions(result) 46 | return@setMethodCallHandler 47 | } 48 | MethodCallFlutter.manageVideoPermissions.call -> { 49 | permissionsManager.manageVideoPermissions(result) 50 | return@setMethodCallHandler 51 | } 52 | MethodCallFlutter.join.call -> { 53 | callResult = join(call) 54 | } 55 | MethodCallFlutter.stop.call -> { 56 | callResult = stop() 57 | } 58 | MethodCallFlutter.mute.call -> { 59 | callResult = mute() 60 | } 61 | MethodCallFlutter.unmute.call -> { 62 | callResult = unmute() 63 | } 64 | MethodCallFlutter.startLocalVideo.call -> { 65 | callResult = startLocalVideo() 66 | } 67 | MethodCallFlutter.stopLocalVideo.call -> { 68 | callResult = stopLocalVideo() 69 | } 70 | MethodCallFlutter.initialAudioSelection.call -> { 71 | callResult = initialAudioSelection() 72 | } 73 | MethodCallFlutter.listAudioDevices.call -> { 74 | callResult = listAudioDevices() 75 | } 76 | MethodCallFlutter.updateAudioDevice.call -> { 77 | callResult = updateAudioDevice(call) 78 | } 79 | else -> callResult = MethodChannelResult(false, Response.method_not_implemented) 80 | } 81 | 82 | if (callResult.result) { 83 | result.success(callResult.toFlutterCompatibleType()) 84 | } else { 85 | result.error( 86 | "Failed", 87 | "MethodChannelHandler failed", 88 | callResult.toFlutterCompatibleType() 89 | ) 90 | } 91 | } 92 | } 93 | 94 | fun callFlutterMethod(method: MethodCallFlutter, args: Any?) { 95 | methodChannel.invokeMethod(method.call, args) 96 | } 97 | 98 | fun join(call: MethodCall): MethodChannelResult { 99 | if (call.arguments == null) { 100 | return MethodChannelResult(false, Response.incorrect_join_response_params.msg) 101 | } 102 | val meetingId: String? = call.argument("MeetingId") 103 | val externalMeetingId: String? = call.argument("ExternalMeetingId") 104 | val mediaRegion: String? = call.argument("MediaRegion") 105 | val audioHostUrl: String? = call.argument("AudioHostUrl") 106 | val audioFallbackUrl: String? = call.argument("AudioFallbackUrl") 107 | val signalingUrl: String? = call.argument("SignalingUrl") 108 | val turnControlUrl: String? = call.argument("TurnControlUrl") 109 | val externalUserId: String? = call.argument("ExternalUserId") 110 | val attendeeId: String? = call.argument("AttendeeId") 111 | val joinToken: String? = call.argument("JoinToken") 112 | 113 | if (meetingId == null || 114 | mediaRegion == null || 115 | audioHostUrl == null || 116 | externalMeetingId == null || 117 | audioFallbackUrl == null || 118 | signalingUrl == null || 119 | turnControlUrl == null || 120 | externalUserId == null || 121 | attendeeId == null || 122 | joinToken == null 123 | ) { 124 | return MethodChannelResult(false, Response.incorrect_join_response_params.msg) 125 | } 126 | 127 | val createMeetingResponse = CreateMeetingResponse( 128 | Meeting( 129 | externalMeetingId, 130 | MediaPlacement(audioFallbackUrl, audioHostUrl, signalingUrl, turnControlUrl), 131 | mediaRegion, 132 | meetingId 133 | ) 134 | ) 135 | val createAttendeeResponse = 136 | CreateAttendeeResponse(Attendee(attendeeId, externalUserId, joinToken)) 137 | val meetingSessionConfiguration = 138 | MeetingSessionConfiguration(createMeetingResponse, createAttendeeResponse) 139 | 140 | val meetingSession = 141 | DefaultMeetingSession(meetingSessionConfiguration, ConsoleLogger(), context) 142 | 143 | MeetingSessionManager.meetingSession = meetingSession 144 | return MeetingSessionManager.startMeeting( 145 | RealtimeObserver(this), 146 | VideoTileObserver(this), 147 | AudioVideoObserver(this) 148 | ) 149 | } 150 | 151 | fun stop(): MethodChannelResult { 152 | return MeetingSessionManager.stop() 153 | } 154 | 155 | fun mute(): MethodChannelResult { 156 | val muted = MeetingSessionManager.meetingSession?.audioVideo?.realtimeLocalMute() 157 | ?: return NULL_MEETING_SESSION_RESPONSE 158 | return if (muted) MethodChannelResult( 159 | true, 160 | Response.mute_successful.msg 161 | ) else MethodChannelResult(false, Response.mute_failed.msg) 162 | } 163 | 164 | fun unmute(): MethodChannelResult { 165 | val unmuted = MeetingSessionManager.meetingSession?.audioVideo?.realtimeLocalUnmute() 166 | ?: return NULL_MEETING_SESSION_RESPONSE 167 | return if (unmuted) MethodChannelResult( 168 | true, 169 | Response.unmute_successful.msg 170 | ) else MethodChannelResult(false, Response.unmute_failed.msg) 171 | } 172 | 173 | fun startLocalVideo(): MethodChannelResult { 174 | MeetingSessionManager.meetingSession?.audioVideo?.startLocalVideo() 175 | ?: return NULL_MEETING_SESSION_RESPONSE 176 | return MethodChannelResult(true, Response.local_video_on_success.msg) 177 | } 178 | 179 | fun stopLocalVideo(): MethodChannelResult { 180 | MeetingSessionManager.meetingSession?.audioVideo?.stopLocalVideo() 181 | ?: return NULL_MEETING_SESSION_RESPONSE 182 | return MethodChannelResult(true, Response.local_video_on_success.msg) 183 | } 184 | 185 | fun initialAudioSelection(): MethodChannelResult { 186 | val device = 187 | MeetingSessionManager.meetingSession?.audioVideo?.getActiveAudioDevice() 188 | ?: return NULL_MEETING_SESSION_RESPONSE 189 | return MethodChannelResult(true, device.label) 190 | } 191 | 192 | fun listAudioDevices(): MethodChannelResult { 193 | val audioDevices = MeetingSessionManager.meetingSession?.audioVideo?.listAudioDevices() 194 | ?: return NULL_MEETING_SESSION_RESPONSE 195 | val transform: (MediaDevice) -> String = { it.label } 196 | return MethodChannelResult(true, audioDevices.map(transform)) 197 | } 198 | 199 | fun updateAudioDevice(call: MethodCall): MethodChannelResult { 200 | val device = 201 | call.arguments ?: return MethodChannelResult(false, Response.null_audio_device.msg) 202 | 203 | val audioDevices = MeetingSessionManager.meetingSession?.audioVideo?.listAudioDevices() 204 | ?: return NULL_MEETING_SESSION_RESPONSE 205 | 206 | for (dev in audioDevices) { 207 | if (device == dev.label) { 208 | MeetingSessionManager.meetingSession?.audioVideo?.chooseAudioDevice(dev) 209 | ?: return MethodChannelResult(false, Response.audio_device_update_failed.msg) 210 | return MethodChannelResult(true, Response.audio_device_updated.msg) 211 | } 212 | } 213 | return MethodChannelResult(false, Response.audio_device_update_failed.msg) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/MethodChannelResult.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | class MethodChannelResult(val result: Boolean, val arguments: Any?) { 9 | 10 | fun toFlutterCompatibleType(): Map { 11 | return mapOf("result" to result, "arguments" to arguments) 12 | } 13 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/PermissionManager.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | import android.Manifest 9 | import android.app.Activity 10 | import android.content.Context 11 | import android.content.pm.PackageManager 12 | import androidx.appcompat.app.AppCompatActivity 13 | import androidx.core.app.ActivityCompat 14 | import androidx.core.content.ContextCompat 15 | import io.flutter.plugin.common.MethodChannel 16 | 17 | class PermissionManager( 18 | val activity: Activity 19 | ) : AppCompatActivity() { 20 | val context: Context 21 | 22 | val VIDEO_PERMISSION_REQUEST_CODE = 1 23 | val VIDEO_PERMISSIONS = arrayOf( 24 | Manifest.permission.CAMERA 25 | ) 26 | 27 | val AUDIO_PERMISSION_REQUEST_CODE = 2 28 | val AUDIO_PERMISSIONS = arrayOf( 29 | Manifest.permission.MODIFY_AUDIO_SETTINGS, 30 | Manifest.permission.RECORD_AUDIO, 31 | ) 32 | 33 | var audioResult: MethodChannel.Result? = null 34 | var videoResult: MethodChannel.Result? = null 35 | 36 | init { 37 | context = activity.applicationContext 38 | } 39 | 40 | fun manageAudioPermissions(result: MethodChannel.Result) { 41 | audioResult = result 42 | if (hasPermissionsAlready(AUDIO_PERMISSIONS)) { 43 | audioCallbackReceived() 44 | } else { 45 | ActivityCompat.requestPermissions( 46 | activity, 47 | AUDIO_PERMISSIONS, 48 | AUDIO_PERMISSION_REQUEST_CODE 49 | ) 50 | } 51 | } 52 | 53 | fun manageVideoPermissions(result: MethodChannel.Result) { 54 | videoResult = result 55 | if (hasPermissionsAlready(VIDEO_PERMISSIONS)) { 56 | videoCallbackReceived() 57 | } else { 58 | ActivityCompat.requestPermissions( 59 | activity, 60 | VIDEO_PERMISSIONS, 61 | VIDEO_PERMISSION_REQUEST_CODE 62 | ) 63 | } 64 | } 65 | 66 | fun audioCallbackReceived() { 67 | val callResult: MethodChannelResult 68 | if (hasPermissionsAlready(AUDIO_PERMISSIONS)) { 69 | callResult = MethodChannelResult(true, Response.audio_auth_granted.msg) 70 | audioResult?.success(callResult.toFlutterCompatibleType()) 71 | } else { 72 | callResult = MethodChannelResult(false, Response.audio_auth_not_granted.msg) 73 | audioResult?.error("Failed", "Permission Error", callResult.toFlutterCompatibleType()) 74 | } 75 | audioResult = null 76 | } 77 | 78 | fun videoCallbackReceived() { 79 | val callResult: MethodChannelResult 80 | if (hasPermissionsAlready(VIDEO_PERMISSIONS)) { 81 | callResult = MethodChannelResult(true, Response.video_auth_granted.msg) 82 | videoResult?.success(callResult.toFlutterCompatibleType()) 83 | } else { 84 | callResult = MethodChannelResult(false, Response.video_auth_not_granted.msg) 85 | videoResult?.error("Failed", "Permission Error", callResult.toFlutterCompatibleType()) 86 | } 87 | videoResult = null 88 | } 89 | 90 | private fun hasPermissionsAlready(PERMISSIONS: Array): Boolean { 91 | return PERMISSIONS.all { 92 | ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/RealtimeObserver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | import com.amazonaws.services.chime.sdk.meetings.realtime.RealtimeObserver 9 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.AttendeeInfo 10 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.SignalUpdate 11 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.VolumeUpdate 12 | 13 | class RealtimeObserver(val methodChannel: MethodChannelCoordinator) : RealtimeObserver { 14 | 15 | override fun onAttendeesDropped(attendeeInfo: Array) { 16 | for (currentAttendeeInfo in attendeeInfo) { 17 | methodChannel.callFlutterMethod( 18 | MethodCall.drop, 19 | attendeeInfoToMap(currentAttendeeInfo) 20 | ) 21 | } 22 | } 23 | 24 | override fun onAttendeesJoined(attendeeInfo: Array) { 25 | for (currentAttendeeInfo in attendeeInfo) { 26 | methodChannel.callFlutterMethod( 27 | MethodCall.join, 28 | attendeeInfoToMap(currentAttendeeInfo) 29 | ) 30 | } 31 | } 32 | 33 | override fun onAttendeesLeft(attendeeInfo: Array) { 34 | for (currentAttendeeInfo in attendeeInfo) { 35 | methodChannel.callFlutterMethod( 36 | MethodCall.leave, 37 | attendeeInfoToMap(currentAttendeeInfo) 38 | ) 39 | } 40 | } 41 | 42 | override fun onAttendeesMuted(attendeeInfo: Array) { 43 | for (currentAttendeeInfo in attendeeInfo) { 44 | methodChannel.callFlutterMethod(MethodCall.mute, attendeeInfoToMap(currentAttendeeInfo)) 45 | } 46 | } 47 | 48 | override fun onAttendeesUnmuted(attendeeInfo: Array) { 49 | for (currentAttendeeInfo in attendeeInfo) { 50 | methodChannel.callFlutterMethod( 51 | MethodCall.unmute, 52 | attendeeInfoToMap(currentAttendeeInfo) 53 | ) 54 | } 55 | } 56 | 57 | override fun onSignalStrengthChanged(signalUpdates: Array) { 58 | // Out of Scope 59 | } 60 | 61 | override fun onVolumeChanged(volumeUpdates: Array) { 62 | // Out of Scope 63 | } 64 | 65 | private fun attendeeInfoToMap(attendee: AttendeeInfo): Map { 66 | return mapOf( 67 | "attendeeId" to attendee.attendeeId, 68 | "externalUserId" to attendee.externalUserId 69 | ) 70 | } 71 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/ResponseEnum.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | enum class Response(val msg: String) { 9 | // Authorization 10 | audio_auth_granted("Android: Audio usage authorized."), 11 | audio_auth_not_granted("Android: Failed to authorize audio."), 12 | video_auth_granted("Android: Video usage authorized."), 13 | video_auth_not_granted("Android: Failed to authorize video."), 14 | 15 | // Meeting 16 | incorrect_join_response_params("Android: ERROR api response has incorrect/missing parameters."), 17 | create_meeting_success("Android: meetingSession created successfully."), 18 | meeting_stopped_successfully("Android: meetingSession stopped successfully."), 19 | meeting_session_is_null("Android: ERROR Meeting session is null."), 20 | 21 | // Mute 22 | mute_successful("Android: Successfully muted user"), 23 | mute_failed("Android: ERROR failed to mute user"), 24 | unmute_successful("Android: Successfully unmuted user"), 25 | unmute_failed("Android: ERROR failed to unmute user"), 26 | 27 | // Video 28 | local_video_on_success("Android: Started local video."), 29 | 30 | // Audio Device 31 | audio_device_updated("Android: Audio device updated"), 32 | audio_device_update_failed("Android: Failed to update audio device."), 33 | null_audio_device("Android: ERROR received null as audio device."), 34 | 35 | // Method Channel 36 | method_not_implemented("Android: ERROR method not implemented.") 37 | } 38 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/VideoTileObserver.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoTileObserver 9 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoTileState 10 | import com.amazonaws.services.chime.sdk.meetings.utils.logger.ConsoleLogger 11 | 12 | class VideoTileObserver(val methodChannel: MethodChannelCoordinator) : VideoTileObserver { 13 | 14 | private val videoTileObserverLogger: ConsoleLogger = ConsoleLogger() 15 | 16 | override fun onVideoTileAdded(tileState: VideoTileState) { 17 | methodChannel.callFlutterMethod(MethodCall.videoTileAdd, videoTileStateToMap(tileState)) 18 | } 19 | 20 | override fun onVideoTilePaused(tileState: VideoTileState) { 21 | // Out of scope 22 | } 23 | 24 | override fun onVideoTileRemoved(tileState: VideoTileState) { 25 | MeetingSessionManager.meetingSession?.audioVideo?.unbindVideoView(tileState.tileId) 26 | ?: videoTileObserverLogger.error( 27 | "onVideoTileRemoved", 28 | "Error while unbinding video view." 29 | ) 30 | methodChannel.callFlutterMethod(MethodCall.videoTileRemove, videoTileStateToMap(tileState)) 31 | } 32 | 33 | override fun onVideoTileResumed(tileState: VideoTileState) { 34 | // Out of scope 35 | } 36 | 37 | override fun onVideoTileSizeChanged(tileState: VideoTileState) { 38 | // Out of scope 39 | } 40 | 41 | private fun videoTileStateToMap(state: VideoTileState): Map { 42 | return mapOf( 43 | "tileId" to state.tileId, 44 | "attendeeId" to state.attendeeId, 45 | "videoStreamContentWidth" to state.videoStreamContentWidth, 46 | "videoStreamContentHeight" to state.videoStreamContentHeight, 47 | "isLocalTile" to state.isLocalTile, 48 | "isContent" to state.isContent 49 | ) 50 | } 51 | } -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/amazonaws/services/chime/flutterdemo/VideoTileView.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | package com.amazonaws.services.chime.flutterdemo 7 | 8 | import android.content.Context 9 | import android.view.View 10 | import io.flutter.plugin.platform.PlatformView 11 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.DefaultVideoRenderView 12 | import com.amazonaws.services.chime.sdk.meetings.audiovideo.video.VideoScalingType 13 | import com.amazonaws.services.chime.sdk.meetings.utils.logger.ConsoleLogger 14 | 15 | internal class VideoTileView(context: Context?, creationParams: Int?) : PlatformView { 16 | private val view: DefaultVideoRenderView 17 | 18 | private val videoTileViewLogger: ConsoleLogger = ConsoleLogger() 19 | 20 | override fun getView(): View { 21 | return view 22 | } 23 | 24 | override fun dispose() {} 25 | 26 | init { 27 | view = DefaultVideoRenderView(context as Context) 28 | view.scalingType = VideoScalingType.AspectFit 29 | MeetingSessionManager.meetingSession?.audioVideo?.bindVideoView(view, creationParams as Int) 30 | ?: videoTileViewLogger.error("VideoTileView", "Error while binding video view.") 31 | } 32 | } -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 8 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | buildscript { 7 | ext.kotlin_version = '1.6.10' 8 | repositories { 9 | google() 10 | mavenCentral() 11 | } 12 | 13 | dependencies { 14 | classpath 'com.android.tools.build:gradle:7.1.2' 15 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 16 | } 17 | } 18 | 19 | allprojects { 20 | repositories { 21 | google() 22 | mavenCentral() 23 | } 24 | } 25 | 26 | rootProject.buildDir = '../build' 27 | subprojects { 28 | project.buildDir = "${rootProject.buildDir}/${project.name}" 29 | } 30 | subprojects { 31 | project.evaluationDependsOn(':app') 32 | } 33 | 34 | task clean(type: Delete) { 35 | delete rootProject.buildDir 36 | } 37 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /android/local.properties: -------------------------------------------------------------------------------- 1 | sdk.dir=/Users/zmauricv/Library/Android/sdk 2 | flutter.sdk=/Users/zmauricv/development/flutter 3 | flutter.buildMode=debug 4 | flutter.versionName=1.0.0 5 | flutter.versionCode=1 -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | include ':app' 7 | 8 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 9 | def properties = new Properties() 10 | 11 | assert localPropertiesFile.exists() 12 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 13 | 14 | def flutterSdkPath = properties.getProperty("flutter.sdk") 15 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 16 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 17 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | com.amazonaws.services.chimesdk.FlutterDemo 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 9.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '11.0' 2 | 3 | target 'Runner' do 4 | # Comment the next line if you don't want to use dynamic frameworks 5 | use_frameworks! 6 | 7 | # Pods for Runner 8 | pod 'AmazonChimeSDK-Bitcode', '~> 0.22.4' 9 | 10 | end 11 | -------------------------------------------------------------------------------- /ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - AmazonChimeSDK-Bitcode (0.22.4): 3 | - AmazonChimeSDKMedia-Bitcode (~> 0.17.8) 4 | - AmazonChimeSDKMedia-Bitcode (0.17.8) 5 | 6 | DEPENDENCIES: 7 | - AmazonChimeSDK-Bitcode (~> 0.22.4) 8 | 9 | SPEC REPOS: 10 | trunk: 11 | - AmazonChimeSDK-Bitcode 12 | - AmazonChimeSDKMedia-Bitcode 13 | 14 | SPEC CHECKSUMS: 15 | AmazonChimeSDK-Bitcode: 897b2c774f9b270ea82957d924ea51c46c64aab2 16 | AmazonChimeSDKMedia-Bitcode: 15ccb22779dbf2b81e097412a8bde5a776d12024 17 | 18 | PODFILE CHECKSUM: 397b826e00422fbfb406095d41d23f7e4b7f1795 19 | 20 | COCOAPODS: 1.11.3 21 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import AmazonChimeSDK 7 | import AmazonChimeSDKMedia 8 | import AVFoundation 9 | import Flutter 10 | import UIKit 11 | 12 | @UIApplicationMain 13 | @objc class AppDelegate: FlutterAppDelegate { 14 | var methodChannel: MethodChannelCoordinator? 15 | override func application( 16 | _ application: UIApplication, 17 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 18 | ) -> Bool { 19 | let controller = window.rootViewController as! FlutterViewController 20 | 21 | let binaryMessenger = controller.binaryMessenger 22 | 23 | methodChannel = MethodChannelCoordinator(binaryMessenger: binaryMessenger) 24 | 25 | methodChannel?.setUpMethodCallHandler() 26 | 27 | let viewFactory = FlutterVideoTileFactory(messenger: binaryMessenger) 28 | 29 | registrar(forPlugin: "AmazonChimeSDKFlutterDemo")?.register(viewFactory, withId: "videoTile") 30 | 31 | GeneratedPluginRegistrant.register(with: self) 32 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-chime-sdk-flutter-demo/3b9335b136b62a72ca2622e544541671d7681598/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | 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. -------------------------------------------------------------------------------- /ios/Runner/AudioVideoObserver.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import AmazonChimeSDK 7 | import AmazonChimeSDKMedia 8 | import Foundation 9 | 10 | class MyAudioVideoObserver: AudioVideoObserver { 11 | 12 | weak var methodChannel: MethodChannelCoordinator? 13 | 14 | init(withMethodChannel methodChannel: MethodChannelCoordinator) { 15 | self.methodChannel = methodChannel 16 | } 17 | 18 | func audioSessionDidStartConnecting(reconnecting: Bool) { 19 | // Out of scope 20 | } 21 | 22 | func audioSessionDidStart(reconnecting: Bool) { 23 | // Out of scope 24 | } 25 | 26 | func audioSessionDidDrop() { 27 | MeetingSession.shared.meetingSession?.logger.info(msg: "Meeting session dropped") 28 | methodChannel?.stopAudioVideoFacadeObservers() 29 | } 30 | 31 | func audioSessionDidStopWithStatus(sessionStatus: MeetingSessionStatus) { 32 | methodChannel?.stopAudioVideoFacadeObservers() 33 | MeetingSession.shared.meetingSession?.logger.info(msg: "Meeting session stopped with status \(sessionStatus.description)") 34 | methodChannel?.callFlutterMethod(method: .audioSessionDidStop, args: nil) 35 | } 36 | 37 | func audioSessionDidCancelReconnect() { 38 | // Out of scope 39 | } 40 | 41 | func connectionDidRecover() { 42 | // Out of scope 43 | } 44 | 45 | func connectionDidBecomePoor() { 46 | // Out of scope 47 | } 48 | 49 | func videoSessionDidStartConnecting() { 50 | MeetingSession.shared.meetingSession?.logger.info(msg: "VideoSession started connecting...") 51 | } 52 | 53 | func videoSessionDidStartWithStatus(sessionStatus: MeetingSessionStatus) { 54 | MeetingSession.shared.meetingSession?.logger.info(msg: 55 | "VideoSession started with status \(sessionStatus.statusCode.description)") 56 | } 57 | 58 | func videoSessionDidStopWithStatus(sessionStatus: MeetingSessionStatus) { 59 | MeetingSession.shared.meetingSession?.logger.info(msg: "VideoSession stopped with status \(sessionStatus.statusCode.description)") 60 | } 61 | 62 | func remoteVideoSourcesDidBecomeAvailable(sources: [RemoteVideoSource]) { 63 | for remoteSourceAvailable in sources { 64 | MeetingSession.shared.meetingSession?.logger.info(msg: "Remote video source became available: \(remoteSourceAvailable.attendeeId)") 65 | } 66 | } 67 | 68 | func remoteVideoSourcesDidBecomeUnavailable(sources: [RemoteVideoSource]) { 69 | for remoteSourcesUnavailable in sources { 70 | MeetingSession.shared.meetingSession?.logger.info(msg: "Remote video source became available: \(remoteSourcesUnavailable.attendeeId)") 71 | } 72 | } 73 | 74 | func cameraSendAvailabilityDidChange(available: Bool) { 75 | // Out of scope 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /ios/Runner/FlutterVideoTileFactory.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import Flutter 7 | import Foundation 8 | 9 | class FlutterVideoTileFactory: NSObject, FlutterPlatformViewFactory { 10 | private let messenger: FlutterBinaryMessenger 11 | 12 | init(messenger: FlutterBinaryMessenger) { 13 | self.messenger = messenger 14 | super.init() 15 | } 16 | 17 | func create( 18 | withFrame frame: CGRect, 19 | viewIdentifier viewId: Int64, 20 | arguments args: Any? 21 | ) -> FlutterPlatformView { 22 | return VideoTileView( 23 | frame: frame, 24 | viewIdentifier: viewId, 25 | arguments: args 26 | ) 27 | } 28 | 29 | public func createArgsCodec() -> FlutterMessageCodec & NSObjectProtocol { 30 | return FlutterStandardMessageCodec.sharedInstance() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | NSCameraUsageDescription 6 | The app needs camera permission for video conferencing 7 | NSMicrophoneUsageDescription 8 | Use microphone to start call 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleDisplayName 12 | Flutter Demo Chime Sdk 13 | CFBundleExecutable 14 | $(EXECUTABLE_NAME) 15 | CFBundleIdentifier 16 | $(PRODUCT_BUNDLE_IDENTIFIER) 17 | CFBundleInfoDictionaryVersion 18 | 6.0 19 | CFBundleName 20 | flutter_demo_chime_sdk 21 | CFBundlePackageType 22 | APPL 23 | CFBundleShortVersionString 24 | $(FLUTTER_BUILD_NAME) 25 | CFBundleSignature 26 | ???? 27 | CFBundleVersion 28 | $(FLUTTER_BUILD_NUMBER) 29 | LSRequiresIPhoneOS 30 | 31 | UILaunchStoryboardName 32 | LaunchScreen 33 | UIMainStoryboardFile 34 | Main 35 | UISupportedInterfaceOrientations 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationLandscapeLeft 39 | UIInterfaceOrientationLandscapeRight 40 | 41 | UISupportedInterfaceOrientations~ipad 42 | 43 | UIInterfaceOrientationPortrait 44 | UIInterfaceOrientationPortraitUpsideDown 45 | UIInterfaceOrientationLandscapeLeft 46 | UIInterfaceOrientationLandscapeRight 47 | 48 | UIViewControllerBasedStatusBarAppearance 49 | 50 | CADisableMinimumFrameDurationOnPhone 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /ios/Runner/MeetingSession.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import AmazonChimeSDK 7 | import AmazonChimeSDKMedia 8 | import AVFoundation 9 | import Flutter 10 | import Foundation 11 | 12 | // Singleton Pattern Class 13 | class MeetingSession { 14 | static let shared = MeetingSession() 15 | 16 | var meetingSession: DefaultMeetingSession? 17 | 18 | let audioVideoConfig = AudioVideoConfiguration() 19 | private let logger = ConsoleLogger(name: "MeetingSession") 20 | 21 | private init() {} 22 | 23 | func startMeetingAudio() -> MethodChannelResponse { 24 | let audioSessionConfigured = configureAudioSession() 25 | let audioSessionStarted = startAudioVideoConnection() 26 | if audioSessionStarted, audioSessionConfigured { 27 | return MethodChannelResponse(result: true, arguments: Response.create_meeting_success.rawValue) 28 | } 29 | return MethodChannelResponse(result: false, arguments: Response.meeting_start_failed.rawValue) 30 | } 31 | 32 | private func startAudioVideoConnection() -> Bool { 33 | do { 34 | try meetingSession?.audioVideo.start() 35 | meetingSession?.audioVideo.startRemoteVideo() 36 | } catch PermissionError.audioPermissionError { 37 | logger.error(msg: "Audio permissions error.") 38 | return false 39 | } catch { 40 | logger.error(msg: "Error starting the Meeting: \(error.localizedDescription)") 41 | return false 42 | } 43 | return true 44 | } 45 | 46 | private func configureAudioSession() -> Bool { 47 | let audioSession = AVAudioSession.sharedInstance() 48 | do { 49 | try audioSession.setActive(true, options: .notifyOthersOnDeactivation) 50 | try audioSession.setMode(.voiceChat) 51 | } catch { 52 | logger.error(msg: "Error configuring AVAudioSession: \(error.localizedDescription)") 53 | return false 54 | } 55 | return true 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ios/Runner/MethodCallEnum.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import Foundation 7 | 8 | enum MethodCall: String { 9 | case manageAudioPermissions 10 | case manageVideoPermissions 11 | case initialAudioSelection 12 | case join 13 | case stop 14 | case leave 15 | case drop 16 | case mute 17 | case unmute 18 | case startLocalVideo 19 | case stopLocalVideo 20 | case videoTileAdd 21 | case videoTileRemove 22 | case listAudioDevices 23 | case updateAudioDevice 24 | case audioSessionDidStop 25 | } 26 | 27 | enum Response: String { 28 | // Authorization 29 | case audio_authorized = "iOS: Audio authorized." 30 | case video_authorized = "iOS: Video authorized." 31 | case video_auth_not_granted = "iOS: ERROR video authorization not granted." 32 | case audio_auth_not_granted = "iOS: ERROR audio authorization not granted." 33 | case audio_restricted = "iOS: ERROR audio restricted." 34 | case video_restricted = "iOS: ERROR video restricted." 35 | case unknown_audio_authorization_status = "iOS: ERROR unknown audio authorization status." 36 | case unknown_video_authorization_status = "iOS: ERROR unknown video authorization status." 37 | 38 | // Meeting 39 | case incorrect_join_response_params = "iOS: ERROR api response has incorrect/missing parameters." 40 | case create_meeting_success = "iOS: meetingSession created successfully." 41 | case create_meeting_failed = "iOS: ERROR failed to create meetingSession." 42 | case meeting_start_failed = "iOS: ERROR failed to start meeting." 43 | case meeting_stopped_successfully = "iOS: meetingSession stopped successfuly." 44 | 45 | // Mute 46 | case mute_successful = "iOS: Successfully muted user" 47 | case mute_failed = "iOS: Could not mute user" 48 | case unmute_successful = "iOS: Successfully unmuted user" 49 | case unmute_failed = "iOS: ERROR Could not unmute user" 50 | 51 | // Video 52 | case local_video_on_success = "iOS: Started local video." 53 | case local_video_on_failed = "iOS: ERROR could not start local video." 54 | case local_video_off_success = "iOS: Stopped local video." 55 | 56 | // Audio Device 57 | case audio_device_updated = "iOS: Audio device updated." 58 | case failed_to_get_initial_audio_device = "iOS: Failed to get initial audio device" 59 | case audio_device_update_failed = "iOS: Failed to update audio device." 60 | case failed_to_list_audio_devices = "iOS: ERROR failed to list audio devices." 61 | 62 | // Method Channel 63 | case method_not_implemented = "iOS: ERROR method not implemented." 64 | } 65 | -------------------------------------------------------------------------------- /ios/Runner/MethodChannelCoordinator.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import AmazonChimeSDK 7 | import AmazonChimeSDKMedia 8 | import AVFoundation 9 | import Flutter 10 | import Foundation 11 | import UIKit 12 | 13 | class MethodChannelCoordinator { 14 | let methodChannel: FlutterMethodChannel 15 | 16 | var realtimeObserver: RealtimeObserver? 17 | 18 | var audioVideoObserver: AudioVideoObserver? 19 | 20 | var videoTileObserver: VideoTileObserver? 21 | 22 | init(binaryMessenger: FlutterBinaryMessenger) { 23 | self.methodChannel = FlutterMethodChannel(name: "com.amazonaws.services.chime.flutterDemo.methodChannel", binaryMessenger: binaryMessenger) 24 | } 25 | 26 | // 27 | // ————————————————————————————————— Method Call Setup ————————————————————————————————— 28 | // 29 | 30 | func setUpMethodCallHandler() { 31 | self.methodChannel.setMethodCallHandler { [unowned self] 32 | (call: FlutterMethodCall, result: @escaping FlutterResult) in 33 | let callMethod = MethodCall(rawValue: call.method) 34 | var response: MethodChannelResponse = .init(result: false, arguments: nil) 35 | switch callMethod { 36 | case .manageAudioPermissions: 37 | response = self.manageAudioPermissions() 38 | case .manageVideoPermissions: 39 | response = self.manageVideoPermissions() 40 | case .join: 41 | response = self.join(call: call) 42 | case .stop: 43 | response = self.stop() 44 | case .mute: 45 | response = self.mute() 46 | case .unmute: 47 | response = self.unmute() 48 | case .startLocalVideo: 49 | response = self.startLocalVideo() 50 | case .stopLocalVideo: 51 | response = self.stopLocalVideo() 52 | case .initialAudioSelection: 53 | response = self.initialAudioSelection() 54 | case .listAudioDevices: 55 | response = self.listAudioDevices() 56 | case .updateAudioDevice: 57 | response = self.updateAudioDevice(call: call) 58 | default: 59 | response = MethodChannelResponse(result: false, arguments: Response.method_not_implemented) 60 | } 61 | result(response.toFlutterCompatibleType()) 62 | } 63 | } 64 | 65 | func callFlutterMethod(method: MethodCall, args: Any?) { 66 | self.methodChannel.invokeMethod(method.rawValue, arguments: args) 67 | } 68 | 69 | // 70 | // ————————————————————————————————— Method Call Options ————————————————————————————————— 71 | // 72 | 73 | func manageAudioPermissions() -> MethodChannelResponse { 74 | let audioPermission = AVAudioSession.sharedInstance().recordPermission 75 | switch audioPermission { 76 | case .undetermined: 77 | if self.requestAudioPermission() { 78 | return MethodChannelResponse(result: true, arguments: Response.audio_authorized.rawValue) 79 | } 80 | return MethodChannelResponse(result: false, arguments: Response.audio_auth_not_granted.rawValue) 81 | case .granted: 82 | return MethodChannelResponse(result: true, arguments: Response.audio_authorized.rawValue) 83 | case .denied: 84 | return MethodChannelResponse(result: false, arguments: Response.audio_auth_not_granted.rawValue) 85 | @unknown default: 86 | return MethodChannelResponse(result: false, arguments: Response.unknown_audio_authorization_status.rawValue) 87 | } 88 | } 89 | 90 | func manageVideoPermissions() -> MethodChannelResponse { 91 | let videoPermission: AVAuthorizationStatus = AVCaptureDevice.authorizationStatus(for: .video) 92 | switch videoPermission { 93 | case .notDetermined: 94 | if self.requestVideoPermission() { 95 | return MethodChannelResponse(result: true, arguments: Response.video_authorized.rawValue) 96 | } 97 | return MethodChannelResponse(result: false, arguments: Response.video_auth_not_granted.rawValue) 98 | case .authorized: 99 | return MethodChannelResponse(result: true, arguments: Response.video_authorized.rawValue) 100 | case .denied: 101 | return MethodChannelResponse(result: false, arguments: Response.video_auth_not_granted.rawValue) 102 | case .restricted: 103 | return MethodChannelResponse(result: false, arguments: Response.video_restricted.rawValue) 104 | @unknown default: 105 | return MethodChannelResponse(result: false, arguments: Response.unknown_video_authorization_status.rawValue) 106 | } 107 | } 108 | 109 | func join(call: FlutterMethodCall) -> MethodChannelResponse { 110 | guard let json = call.arguments as? [String: String] else { 111 | return MethodChannelResponse(result: false, arguments: Response.create_meeting_failed) 112 | } 113 | 114 | // TODO: zmauricv: add a Json Decoder 115 | guard let meetingId = json["MeetingId"], let externalMeetingId = json["ExternalMeetingId"], let mediaRegion = json["MediaRegion"], let audioHostUrl = json["AudioHostUrl"], let audioFallbackUrl = json["AudioFallbackUrl"], let signalingUrl = json["SignalingUrl"], let turnControlUrl = json["TurnControlUrl"], let externalUserId = json["ExternalUserId"], let attendeeId = json["AttendeeId"], let joinToken = json["JoinToken"] 116 | else { 117 | return MethodChannelResponse(result: false, arguments: Response.incorrect_join_response_params.rawValue) 118 | } 119 | 120 | let meetingResponse = CreateMeetingResponse(meeting: Meeting(externalMeetingId: externalMeetingId, mediaPlacement: MediaPlacement(audioFallbackUrl: audioFallbackUrl, audioHostUrl: audioHostUrl, signalingUrl: signalingUrl, turnControlUrl: turnControlUrl), mediaRegion: mediaRegion, meetingId: meetingId)) 121 | 122 | let attendeeResponse = CreateAttendeeResponse(attendee: Attendee(attendeeId: attendeeId, externalUserId: externalUserId, joinToken: joinToken)) 123 | 124 | let meetingSessionConfiguration = MeetingSessionConfiguration(createMeetingResponse: meetingResponse, createAttendeeResponse: attendeeResponse) 125 | 126 | let logger = ConsoleLogger(name: "MeetingSession Logger", level: LogLevel.DEBUG) 127 | 128 | let meetingSession = DefaultMeetingSession(configuration: meetingSessionConfiguration, logger: logger) 129 | 130 | self.configureAudioSession() 131 | 132 | // Update Singleton Class 133 | MeetingSession.shared.meetingSession = meetingSession 134 | 135 | self.setupAudioVideoFacadeObservers() 136 | let meetingStartResponse = MeetingSession.shared.startMeetingAudio() 137 | 138 | return meetingStartResponse 139 | } 140 | 141 | func stop() -> MethodChannelResponse { 142 | MeetingSession.shared.meetingSession?.audioVideo.stop() 143 | MeetingSession.shared.meetingSession = nil 144 | return MethodChannelResponse(result: true, arguments: Response.meeting_stopped_successfully.rawValue) 145 | } 146 | 147 | func mute() -> MethodChannelResponse { 148 | let muted = MeetingSession.shared.meetingSession?.audioVideo.realtimeLocalMute() ?? false 149 | 150 | if muted { 151 | return MethodChannelResponse(result: true, arguments: Response.mute_successful.rawValue) 152 | } else { 153 | return MethodChannelResponse(result: false, arguments: Response.mute_failed.rawValue) 154 | } 155 | } 156 | 157 | func unmute() -> MethodChannelResponse { 158 | let unmuted = MeetingSession.shared.meetingSession?.audioVideo.realtimeLocalUnmute() ?? false 159 | 160 | if unmuted { 161 | return MethodChannelResponse(result: true, arguments: Response.unmute_successful.rawValue) 162 | } else { 163 | return MethodChannelResponse(result: false, arguments: Response.unmute_successful.rawValue) 164 | } 165 | } 166 | 167 | func startLocalVideo() -> MethodChannelResponse { 168 | do { 169 | try MeetingSession.shared.meetingSession?.audioVideo.startLocalVideo() 170 | return MethodChannelResponse(result: true, arguments: Response.local_video_on_success.rawValue) 171 | } catch { 172 | MeetingSession.shared.meetingSession?.logger.error(msg: "Error configuring AVAudioSession: \(error.localizedDescription)") 173 | return MethodChannelResponse(result: false, arguments: Response.local_video_on_failed.rawValue) 174 | } 175 | } 176 | 177 | func stopLocalVideo() -> MethodChannelResponse { 178 | MeetingSession.shared.meetingSession?.audioVideo.stopLocalVideo() 179 | return MethodChannelResponse(result: true, arguments: Response.local_video_off_success.rawValue) 180 | } 181 | 182 | func initialAudioSelection() -> MethodChannelResponse { 183 | if let initialAudioDevice = MeetingSession.shared.meetingSession?.audioVideo.getActiveAudioDevice() { 184 | return MethodChannelResponse(result: true, arguments: initialAudioDevice.label) 185 | } 186 | return MethodChannelResponse(result: false, arguments: Response.failed_to_get_initial_audio_device.rawValue) 187 | } 188 | 189 | func listAudioDevices() -> MethodChannelResponse { 190 | guard let audioDevices = MeetingSession.shared.meetingSession?.audioVideo.listAudioDevices() else { 191 | return MethodChannelResponse(result: false, arguments: Response.failed_to_list_audio_devices.rawValue) 192 | } 193 | 194 | return MethodChannelResponse(result: true, arguments: audioDevices.map { $0.label }) 195 | } 196 | 197 | func updateAudioDevice(call: FlutterMethodCall) -> MethodChannelResponse { 198 | guard let device = call.arguments as? String else { 199 | return MethodChannelResponse(result: false, arguments: Response.audio_device_update_failed.rawValue) 200 | } 201 | 202 | guard let audioDevices = MeetingSession.shared.meetingSession?.audioVideo.listAudioDevices() else { 203 | MeetingSession.shared.meetingSession?.logger.error(msg: Response.failed_to_list_audio_devices.rawValue) 204 | return MethodChannelResponse(result: false, arguments: Response.failed_to_list_audio_devices.rawValue) 205 | } 206 | 207 | for dev in audioDevices { 208 | if device == dev.label { 209 | MeetingSession.shared.meetingSession?.audioVideo.chooseAudioDevice(mediaDevice: dev) 210 | return MethodChannelResponse(result: true, arguments: Response.audio_device_updated.rawValue) 211 | } 212 | } 213 | 214 | return MethodChannelResponse(result: false, arguments: Response.audio_device_update_failed.rawValue) 215 | } 216 | 217 | // 218 | // ————————————————————————————————— Helper Functions ————————————————————————————————— 219 | // 220 | 221 | private func requestAudioPermission() -> Bool { 222 | var result = false 223 | 224 | let group = DispatchGroup() 225 | group.enter() 226 | DispatchQueue.global(qos: .default).async { 227 | AVAudioSession.sharedInstance().requestRecordPermission { granted in 228 | result = granted 229 | group.leave() 230 | } 231 | } 232 | group.wait() 233 | return result 234 | } 235 | 236 | private func requestVideoPermission() -> Bool { 237 | var result = false 238 | 239 | let group = DispatchGroup() 240 | group.enter() 241 | DispatchQueue.global(qos: .default).async { 242 | AVCaptureDevice.requestAccess(for: .video) { granted in 243 | result = granted 244 | group.leave() 245 | } 246 | } 247 | group.wait() 248 | return result 249 | } 250 | 251 | private func setupAudioVideoFacadeObservers() { 252 | self.realtimeObserver = MyRealtimeObserver(withMethodChannel: self) 253 | if self.realtimeObserver != nil { 254 | MeetingSession.shared.meetingSession?.audioVideo.addRealtimeObserver(observer: self.realtimeObserver!) 255 | MeetingSession.shared.meetingSession?.logger.info(msg: "realtimeObserver set up...") 256 | } 257 | 258 | self.audioVideoObserver = MyAudioVideoObserver(withMethodChannel: self) 259 | if self.audioVideoObserver != nil { 260 | MeetingSession.shared.meetingSession?.audioVideo.addAudioVideoObserver(observer: self.audioVideoObserver!) 261 | MeetingSession.shared.meetingSession?.logger.info(msg: "audioVideoObserver set up...") 262 | } 263 | 264 | self.videoTileObserver = MyVideoTileObserver(withMethodChannel: self) 265 | if self.videoTileObserver != nil { 266 | MeetingSession.shared.meetingSession?.audioVideo.addVideoTileObserver(observer: self.videoTileObserver!) 267 | MeetingSession.shared.meetingSession?.logger.info(msg: "VideoTileObserver set up...") 268 | } 269 | } 270 | 271 | func stopAudioVideoFacadeObservers() { 272 | if let rtObserver = self.realtimeObserver { 273 | MeetingSession.shared.meetingSession?.audioVideo.removeRealtimeObserver(observer: rtObserver) 274 | } 275 | 276 | if let avObserver = self.audioVideoObserver { 277 | MeetingSession.shared.meetingSession?.audioVideo.removeAudioVideoObserver(observer: avObserver) 278 | } 279 | 280 | if let vtObserver = self.videoTileObserver { 281 | MeetingSession.shared.meetingSession?.audioVideo.removeVideoTileObserver(observer: vtObserver) 282 | } 283 | } 284 | 285 | private func configureAudioSession() { 286 | let audioSession = AVAudioSession.sharedInstance() 287 | do { 288 | if audioSession.category != .playAndRecord { 289 | try audioSession.setCategory(AVAudioSession.Category.playAndRecord, 290 | options: AVAudioSession.CategoryOptions.allowBluetooth) 291 | try audioSession.setActive(true, options: .notifyOthersOnDeactivation) 292 | } 293 | if audioSession.mode != .voiceChat { 294 | try audioSession.setMode(.voiceChat) 295 | } 296 | } catch { 297 | MeetingSession.shared.meetingSession?.logger.error(msg: "Error configuring AVAudioSession: \(error.localizedDescription)") 298 | } 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /ios/Runner/MethodChannelResponse.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import Foundation 7 | 8 | class MethodChannelResponse { 9 | let result: Bool 10 | let arguments: Any? 11 | 12 | init(result res: Bool, arguments args: Any?) { 13 | self.result = res 14 | self.arguments = args 15 | } 16 | 17 | func toFlutterCompatibleType() -> [String: Any?] { 18 | return ["result": result, "arguments": arguments] 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ios/Runner/RealtimeObserver.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import AmazonChimeSDK 7 | import AmazonChimeSDKMedia 8 | import AVFoundation 9 | import Flutter 10 | import Foundation 11 | import UIKit 12 | 13 | class MyRealtimeObserver: RealtimeObserver { 14 | weak var methodChannel: MethodChannelCoordinator? 15 | 16 | init(withMethodChannel methodChannel: MethodChannelCoordinator) { 17 | self.methodChannel = methodChannel 18 | } 19 | 20 | func volumeDidChange(volumeUpdates: [VolumeUpdate]) { 21 | // Out of scope 22 | } 23 | 24 | func signalStrengthDidChange(signalUpdates: [SignalUpdate]) { 25 | // Out of scope 26 | } 27 | 28 | func attendeesDidJoin(attendeeInfo: [AttendeeInfo]) { 29 | for currentAttendeeInfo in attendeeInfo { 30 | methodChannel?.callFlutterMethod(method: .join, args: attendeeInfoToDictionary(attendeeInfo: currentAttendeeInfo)) 31 | } 32 | } 33 | 34 | func attendeesDidLeave(attendeeInfo: [AttendeeInfo]) { 35 | for currentAttendeeInfo in attendeeInfo { 36 | methodChannel?.callFlutterMethod(method: .leave, args: attendeeInfoToDictionary(attendeeInfo: currentAttendeeInfo)) 37 | } 38 | } 39 | 40 | func attendeesDidDrop(attendeeInfo: [AttendeeInfo]) { 41 | for currentAttendeeInfo in attendeeInfo { 42 | methodChannel?.callFlutterMethod(method: .drop, args: attendeeInfoToDictionary(attendeeInfo: currentAttendeeInfo)) 43 | } 44 | } 45 | 46 | func attendeesDidMute(attendeeInfo: [AttendeeInfo]) { 47 | for currentAttendeeInfo in attendeeInfo { 48 | methodChannel?.callFlutterMethod(method: .mute, args: attendeeInfoToDictionary(attendeeInfo: currentAttendeeInfo)) 49 | } 50 | } 51 | 52 | func attendeesDidUnmute(attendeeInfo: [AttendeeInfo]) { 53 | for currentAttendeeInfo in attendeeInfo { 54 | methodChannel?.callFlutterMethod(method: .unmute, args: attendeeInfoToDictionary(attendeeInfo: currentAttendeeInfo)) 55 | } 56 | } 57 | 58 | private func attendeeInfoToDictionary(attendeeInfo: AttendeeInfo) -> [String: String] { 59 | return [ 60 | "attendeeId": attendeeInfo.attendeeId, 61 | "externalUserId": attendeeInfo.externalUserId 62 | ] 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ios/Runner/ResponseEnums.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import Foundation 7 | 8 | enum Response : String{ 9 | case video_auth_not_granted = "iOS: ERROR video authorization not granted." 10 | case audio_auth_not_granted = "iOS: ERROR audio authorization not granted." 11 | case create_meeting_failed = "iOS: ERROR failed to create meetingSession." 12 | case create_meeting_success = "iOS: meetingSession created successfully." 13 | case meeting_start_failed = "iOS: ERROR failed to start meeting." 14 | case incorrect_join_response_params = "iOS: ERROR api response has incorrect/missing parameters." 15 | case meeting_stopped_successfully = "iOS: meetingSession successfuly stopped." 16 | } 17 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /ios/Runner/VideoTileObserver.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import AmazonChimeSDK 7 | import AmazonChimeSDKMedia 8 | import Foundation 9 | 10 | class MyVideoTileObserver: VideoTileObserver { 11 | weak var methodChannel: MethodChannelCoordinator? 12 | 13 | init(withMethodChannel methodChannel: MethodChannelCoordinator) { 14 | self.methodChannel = methodChannel 15 | } 16 | 17 | func videoTileDidAdd(tileState: VideoTileState) { 18 | methodChannel?.callFlutterMethod(method: .videoTileAdd, args: videoTileStateToDict(state: tileState)) 19 | } 20 | 21 | func videoTileDidRemove(tileState: VideoTileState) { 22 | MeetingSession.shared.meetingSession?.audioVideo.unbindVideoView(tileId: tileState.tileId) 23 | methodChannel?.callFlutterMethod(method: .videoTileRemove, args: videoTileStateToDict(state: tileState)) 24 | } 25 | 26 | func videoTileDidPause(tileState: VideoTileState) { 27 | // Out of Scope 28 | } 29 | 30 | func videoTileDidResume(tileState: VideoTileState) { 31 | // Out of Scope 32 | } 33 | 34 | func videoTileSizeDidChange(tileState: VideoTileState) { 35 | // Out of Scope 36 | } 37 | 38 | private func videoTileStateToDict(state: VideoTileState) -> [String: Any?] { 39 | return [ 40 | "tileId": state.tileId, 41 | "attendeeId": state.attendeeId, 42 | "videoStreamContentWidth": state.videoStreamContentWidth, 43 | "videoStreamContentHeight": state.videoStreamContentHeight, 44 | "isLocalTile": state.isLocalTile, 45 | "isContent": state.isContent 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /ios/Runner/VideoTileView.swift: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import AmazonChimeSDK 7 | import Foundation 8 | 9 | class VideoTileView: NSObject, FlutterPlatformView { 10 | private var _view: UIView 11 | 12 | init( 13 | frame: CGRect, 14 | viewIdentifier viewId: Int64, 15 | arguments args: Any? 16 | ) { 17 | _view = DefaultVideoRenderView() 18 | super.init() 19 | 20 | // Receieve tileId as a param. 21 | let tileId = args as! Int 22 | let videoRenderView = _view as! VideoRenderView 23 | 24 | // Bind view to VideoView 25 | MeetingSession.shared.meetingSession?.audioVideo.bindVideoView(videoView: videoRenderView, tileId: tileId) 26 | 27 | // Fix aspect ratio 28 | _view.contentMode = .scaleAspectFit 29 | 30 | // Declare _view as UIView for Flutter interpretation 31 | _view = _view as UIView 32 | } 33 | 34 | func view() -> UIView { 35 | return _view 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lib/api.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import 'dart:convert'; 7 | import 'package:flutter_demo_chime_sdk/api_config.dart'; 8 | import 'package:http/http.dart' as http; 9 | 10 | import 'logger.dart'; 11 | 12 | class Api { 13 | final String _baseUrl = ApiConfig.apiUrl; 14 | final String _region = ApiConfig.region; 15 | 16 | Future join(String meetingId, String attendeeId) async { 17 | String url = "${_baseUrl}join?title=$meetingId&name=$attendeeId®ion=$_region"; 18 | 19 | try { 20 | final http.Response response = await http.post(Uri.parse(url)); 21 | 22 | logger.d("STATUS: ${response.statusCode}"); 23 | 24 | if (response.statusCode >= 200 && response.statusCode < 300) { 25 | logger.i("POST - join api call successful!"); 26 | Map joinInfoMap = jsonDecode(response.body); 27 | JoinInfo joinInfo = JoinInfo.fromJson(joinInfoMap); 28 | return ApiResponse(response: true, content: joinInfo); 29 | } 30 | } catch (e) { 31 | logger.e("join request Failed. Status: ${e.toString()}"); 32 | return ApiResponse(response: false, error: e.toString()); 33 | } 34 | return null; 35 | } 36 | 37 | Map joinInfoToJSON(JoinInfo info) { 38 | Map flattenedJSON = { 39 | "MeetingId": info.meeting.meetingId, 40 | "ExternalMeetingId": info.meeting.externalMeetingId, 41 | "MediaRegion": info.meeting.mediaRegion, 42 | "AudioHostUrl": info.meeting.mediaPlacement.audioHostUrl, 43 | "AudioFallbackUrl": info.meeting.mediaPlacement.audioFallbackUrl, 44 | "SignalingUrl": info.meeting.mediaPlacement.signalingUrl, 45 | "TurnControlUrl": info.meeting.mediaPlacement.turnControllerUrl, 46 | "ExternalUserId": info.attendee.externalUserId, 47 | "AttendeeId": info.attendee.attendeeId, 48 | "JoinToken": info.attendee.joinToken 49 | }; 50 | 51 | return flattenedJSON; 52 | } 53 | } 54 | 55 | class JoinInfo { 56 | final Meeting meeting; 57 | 58 | final AttendeeInfo attendee; 59 | 60 | JoinInfo(this.meeting, this.attendee); 61 | 62 | factory JoinInfo.fromJson(Map json) { 63 | return JoinInfo(Meeting.fromJson(json), AttendeeInfo.fromJson(json)); 64 | } 65 | } 66 | 67 | class Meeting { 68 | final String meetingId; 69 | final String externalMeetingId; 70 | final String mediaRegion; 71 | final MediaPlacement mediaPlacement; 72 | 73 | Meeting(this.meetingId, this.externalMeetingId, this.mediaRegion, this.mediaPlacement); 74 | 75 | factory Meeting.fromJson(Map json) { 76 | // TODO zmauricv: Look into JSON Serialization Solutions 77 | var meetingMap = json['JoinInfo']['Meeting']['Meeting']; 78 | 79 | return Meeting( 80 | meetingMap['MeetingId'], 81 | meetingMap['ExternalMeetingId'], 82 | meetingMap['MediaRegion'], 83 | MediaPlacement.fromJson(json), 84 | ); 85 | } 86 | } 87 | 88 | class MediaPlacement { 89 | final String audioHostUrl; 90 | final String audioFallbackUrl; 91 | final String signalingUrl; 92 | final String turnControllerUrl; 93 | 94 | MediaPlacement(this.audioHostUrl, this.audioFallbackUrl, this.signalingUrl, this.turnControllerUrl); 95 | 96 | factory MediaPlacement.fromJson(Map json) { 97 | var mediaPlacementMap = json['JoinInfo']['Meeting']['Meeting']['MediaPlacement']; 98 | return MediaPlacement(mediaPlacementMap['AudioHostUrl'], mediaPlacementMap['AudioFallbackUrl'], 99 | mediaPlacementMap['SignalingUrl'], mediaPlacementMap['TurnControlUrl']); 100 | } 101 | } 102 | 103 | class AttendeeInfo { 104 | final String externalUserId; 105 | final String attendeeId; 106 | final String joinToken; 107 | 108 | AttendeeInfo(this.externalUserId, this.attendeeId, this.joinToken); 109 | 110 | factory AttendeeInfo.fromJson(Map json) { 111 | var attendeeMap = json['JoinInfo']['Attendee']['Attendee']; 112 | 113 | return AttendeeInfo(attendeeMap['ExternalUserId'], attendeeMap['AttendeeId'], attendeeMap['JoinToken']); 114 | } 115 | } 116 | 117 | class ApiResponse { 118 | final bool response; 119 | final JoinInfo? content; 120 | final String? error; 121 | 122 | ApiResponse({required this.response, this.content, this.error}); 123 | } 124 | -------------------------------------------------------------------------------- /lib/api_config.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | class ApiConfig { 7 | // Format: https://.execute-api..amazonaws.com/Prod/ 8 | static String get apiUrl => ""; // API url goes here 9 | static String get region => ""; // Add region here 10 | } 11 | -------------------------------------------------------------------------------- /lib/attendee.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import 'package:flutter_demo_chime_sdk/video_tile.dart'; 7 | 8 | class Attendee { 9 | final String attendeeId; 10 | final String externalUserId; 11 | 12 | bool muteStatus = false; 13 | bool isVideoOn = false; 14 | 15 | VideoTile? videoTile; 16 | 17 | Attendee(this.attendeeId, this.externalUserId); 18 | 19 | factory Attendee.fromJson(dynamic json) { 20 | return Attendee(json["attendeeId"], json["externalUserId"]); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lib/interfaces/audio_devices_interface.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | class AudioDevicesInterface { 7 | void initialAudioSelection() { 8 | // Gets initial selected audio device 9 | } 10 | 11 | void listAudioDevices() async { 12 | // Gets a list of available audio devices. 13 | } 14 | 15 | void updateCurrentDevice(String device) async { 16 | // Updates the current audio device to the chosen audio device. 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/interfaces/audio_video_interface.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | class AudioVideoInterface { 7 | void audioSessionDidStop() { 8 | // Called when audio session is stopped 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /lib/interfaces/realtime_interface.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import '../attendee.dart'; 7 | 8 | class RealtimeInterface { 9 | void attendeeDidJoin(Attendee attendee) { 10 | // Gets called when an attendee joins the meeting 11 | } 12 | 13 | void attendeeDidLeave(Attendee attendee, {required bool didDrop}) { 14 | // Gets called when an attendee leaves or drops the meeting 15 | } 16 | 17 | void attendeeDidMute(Attendee attendee) { 18 | // Gets called when an mutes themselves 19 | } 20 | 21 | void attendeeDidUnmute(Attendee attendee) { 22 | // Gets called when an unmutes themselves 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /lib/interfaces/video_tile_interface.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import '../video_tile.dart'; 7 | 8 | class VideoTileInterface { 9 | void videoTileDidAdd(String attendeeId, VideoTile videoTile) { 10 | // Gets called when a video tile is added 11 | } 12 | 13 | void videoTileDidRemove(String attendeeId, VideoTile videoTile) { 14 | // Gets called when a video tile is removed 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /lib/logger.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import 'package:logger/logger.dart'; 7 | 8 | final logger = Logger( 9 | printer: PrettyPrinter( 10 | methodCount: 0, 11 | errorMethodCount: 5, 12 | lineLength: 50, 13 | colors: true, 14 | printEmojis: true, 15 | printTime: true, 16 | ), 17 | ); 18 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_demo_chime_sdk/view_models/join_meeting_view_model.dart'; 8 | import 'package:flutter_demo_chime_sdk/view_models/meeting_view_model.dart'; 9 | import 'package:flutter_demo_chime_sdk/views/join_meeting.dart'; 10 | import 'package:flutter_demo_chime_sdk/views/meeting.dart'; 11 | import 'package:provider/provider.dart'; 12 | 13 | import 'method_channel_coordinator.dart'; 14 | 15 | void main() { 16 | runApp(const MyApp()); 17 | } 18 | 19 | class MyApp extends StatelessWidget { 20 | const MyApp({Key? key}) : super(key: key); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | return MultiProvider( 25 | providers: [ 26 | ChangeNotifierProvider(create: (_) => MethodChannelCoordinator()), 27 | ChangeNotifierProvider(create: (_) => JoinMeetingViewModel()), 28 | ChangeNotifierProvider(create: (context) => MeetingViewModel(context)), 29 | ], 30 | child: GestureDetector( 31 | onTap: () { 32 | // Unfocus keyboard when tapping on non-clickable widget 33 | FocusScopeNode currentFocus = FocusScope.of(context); 34 | if (!currentFocus.hasPrimaryFocus) { 35 | FocusManager.instance.primaryFocus?.unfocus(); 36 | } 37 | }, 38 | child: MaterialApp( 39 | title: 'Amazon Chime SDK Flutter Demo', 40 | theme: ThemeData( 41 | primarySwatch: Colors.blue, 42 | ), 43 | routes: { 44 | '/joinMeeting': (_) => JoinMeetingView(), 45 | '/meeting': (_) => const MeetingView(), 46 | }, 47 | home: JoinMeetingView(), 48 | ), 49 | ), 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /lib/method_channel_coordinator.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter/services.dart'; 8 | import 'package:flutter_demo_chime_sdk/attendee.dart'; 9 | import 'package:flutter_demo_chime_sdk/interfaces/audio_video_interface.dart'; 10 | import 'package:flutter_demo_chime_sdk/interfaces/realtime_interface.dart'; 11 | import 'package:flutter_demo_chime_sdk/interfaces/video_tile_interface.dart'; 12 | import 'package:flutter_demo_chime_sdk/response_enums.dart'; 13 | import 'package:flutter_demo_chime_sdk/video_tile.dart'; 14 | import 'package:flutter_demo_chime_sdk/view_models/meeting_view_model.dart'; 15 | 16 | import 'attendee.dart'; 17 | import 'interfaces/realtime_interface.dart'; 18 | import 'logger.dart'; 19 | import 'video_tile.dart'; 20 | 21 | class MethodChannelCoordinator extends ChangeNotifier { 22 | final MethodChannel methodChannel = const MethodChannel("com.amazonaws.services.chime.flutterDemo.methodChannel"); 23 | 24 | RealtimeInterface? realtimeObserver; 25 | VideoTileInterface? videoTileObserver; 26 | AudioVideoInterface? audioVideoObserver; 27 | 28 | void initializeMethodCallHandler() { 29 | methodChannel.setMethodCallHandler(methodCallHandler); 30 | logger.i("Flutter Method Call Handler initialized."); 31 | } 32 | 33 | void initializeRealtimeObserver(RealtimeInterface realtimeInterface) { 34 | realtimeObserver = realtimeInterface; 35 | } 36 | 37 | void initializeAudioVideoObserver(AudioVideoInterface audioVideoInterface) { 38 | audioVideoObserver = audioVideoInterface; 39 | } 40 | 41 | void initializeVideoTileObserver(VideoTileInterface videoTileInterface) { 42 | videoTileObserver = videoTileInterface; 43 | } 44 | 45 | void initializeObservers(MeetingViewModel meetingProvider) { 46 | initializeRealtimeObserver(meetingProvider); 47 | initializeAudioVideoObserver(meetingProvider); 48 | initializeVideoTileObserver(meetingProvider); 49 | logger.d("Observers initialized"); 50 | } 51 | 52 | Future callMethod(String methodName, [dynamic args]) async { 53 | logger.d("Calling $methodName through method channel with args: $args"); 54 | try { 55 | dynamic response = await methodChannel.invokeMethod(methodName, args); 56 | return MethodChannelResponse.fromJson(response); 57 | } catch (e) { 58 | logger.e(e.toString()); 59 | return MethodChannelResponse(false, null); 60 | } 61 | } 62 | 63 | Future methodCallHandler(MethodCall call) async { 64 | logger.d("Recieved method call ${call.method} with arguments: ${call.arguments}"); 65 | 66 | switch (call.method) { 67 | case MethodCallOption.join: 68 | final Attendee attendee = Attendee.fromJson(call.arguments); 69 | realtimeObserver?.attendeeDidJoin(attendee); 70 | break; 71 | case MethodCallOption.leave: 72 | final Attendee attendee = Attendee.fromJson(call.arguments); 73 | realtimeObserver?.attendeeDidLeave(attendee, didDrop: false); 74 | break; 75 | case MethodCallOption.drop: 76 | final Attendee attendee = Attendee.fromJson(call.arguments); 77 | realtimeObserver?.attendeeDidLeave(attendee, didDrop: true); 78 | break; 79 | case MethodCallOption.mute: 80 | final Attendee attendee = Attendee.fromJson(call.arguments); 81 | realtimeObserver?.attendeeDidMute(attendee); 82 | break; 83 | case MethodCallOption.unmute: 84 | final Attendee attendee = Attendee.fromJson(call.arguments); 85 | realtimeObserver?.attendeeDidUnmute(attendee); 86 | break; 87 | case MethodCallOption.videoTileAdd: 88 | final String attendeeId = call.arguments["attendeeId"]; 89 | final VideoTile videoTile = VideoTile.fromJson(call.arguments); 90 | videoTileObserver?.videoTileDidAdd(attendeeId, videoTile); 91 | break; 92 | case MethodCallOption.videoTileRemove: 93 | final String attendeeId = call.arguments["attendeeId"]; 94 | final VideoTile videoTile = VideoTile.fromJson(call.arguments); 95 | videoTileObserver?.videoTileDidRemove(attendeeId, videoTile); 96 | break; 97 | case MethodCallOption.audioSessionDidStop: 98 | audioVideoObserver?.audioSessionDidStop(); 99 | break; 100 | default: 101 | logger.w("Method ${call.method} with args ${call.arguments} does not exist"); 102 | } 103 | } 104 | } 105 | 106 | class MethodChannelResponse { 107 | late bool result; 108 | dynamic arguments; 109 | 110 | MethodChannelResponse(this.result, this.arguments); 111 | 112 | factory MethodChannelResponse.fromJson(dynamic json) { 113 | return MethodChannelResponse(json["result"], json["arguments"]); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /lib/response_enums.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | // ignore_for_file: constant_identifier_names 7 | 8 | class Response { 9 | static const String audio_and_video_permission_denied = "ERROR audio and video permissions not authorized."; 10 | static const String audio_not_authorized = "ERROR audio permissions not authorized."; 11 | static const String video_not_authorized = "ERROR video permissions not authorized."; 12 | 13 | // API 14 | static const String not_connected_to_internet = "ERROR device is not connected to the internet."; 15 | static const String api_response_null = "ERROR api response is null"; 16 | static const String api_call_failed = "ERROR api call has returned incorrect status"; 17 | 18 | // Meeting 19 | static const String empty_parameter = "ERROR empty meeting or attendee"; 20 | static const String invalid_attendee_or_meeting = "ERROR meeting or attendee are too short or long."; 21 | static const String null_join_response = "ERROR join response is null."; 22 | static const String null_meeting_data = "ERROR meeting data is null"; 23 | static const String null_local_attendee = "ERROR local attendee is null"; 24 | static const String null_remote_attendee = "ERROR remote attendee is null"; 25 | static const String stop_response_null = "ERROR stop response is null"; 26 | 27 | // Observers 28 | static const String null_realtime_observers = "WARNING realtime observer is null"; 29 | static const String null_audiovideo_observers = "WARNING audiovideo observer is null"; 30 | static const String null_videotile_observers = "WARNING videotile observer is null"; 31 | 32 | // Mute 33 | static const String mute_response_null = "ERROR mute response is null."; 34 | static const String unmute_response_null = "ERROR unmute response is null."; 35 | 36 | // Video 37 | static const String video_start_response_null = "ERROR video start response is null"; 38 | static const String video_stopped_response_null = "ERROR video stop response is null"; 39 | 40 | // Audio Device 41 | static const String null_initial_audio_device = "ERROR failed to get initial audio device"; 42 | static const String null_audio_device_list = "ERROR audio device list is null"; 43 | static const String null_audio_device_update = "ERROR audio device update is null."; 44 | } 45 | 46 | class MethodCallOption { 47 | static const String manageAudioPermissions = "manageAudioPermissions"; 48 | static const String manageVideoPermissions = "manageVideoPermissions"; 49 | static const String initialAudioSelection = "initialAudioSelection"; 50 | static const String join = "join"; 51 | static const String stop = "stop"; 52 | static const String leave = "leave"; 53 | static const String drop = "drop"; 54 | static const String mute = "mute"; 55 | static const String unmute = "unmute"; 56 | static const String localVideoOn = "startLocalVideo"; 57 | static const String localVideoOff = "stopLocalVideo"; 58 | static const String videoTileAdd = "videoTileAdd"; 59 | static const String videoTileRemove = "videoTileRemove"; 60 | static const String listAudioDevices = "listAudioDevices"; 61 | static const String updateAudioDevice = "updateAudioDevice"; 62 | static const String audioSessionDidDrop = "audioSessionDidDrop"; 63 | static const String audioSessionDidStop = "audioSessionDidStop"; 64 | } 65 | -------------------------------------------------------------------------------- /lib/video_tile.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | class VideoTile { 7 | final int tileId; 8 | int videoStreamContentWidth; 9 | int videoStreamContentHeight; 10 | bool isLocalTile; 11 | bool isContentShare; 12 | 13 | VideoTile(this.tileId, this.videoStreamContentWidth, this.videoStreamContentHeight, this.isLocalTile, this.isContentShare); 14 | 15 | factory VideoTile.fromJson(json) { 16 | return VideoTile(json["tileId"], json["videoStreamContentWidth"], json["videoStreamContentHeight"], json["isLocalTile"], 17 | json["isContent"]); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/view_models/join_meeting_view_model.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_demo_chime_sdk/api.dart'; 8 | 9 | import '../method_channel_coordinator.dart'; 10 | import '../response_enums.dart'; 11 | import '../logger.dart'; 12 | import 'meeting_view_model.dart'; 13 | import 'dart:io' show InternetAddress, SocketException; 14 | 15 | class JoinMeetingViewModel extends ChangeNotifier { 16 | final Api api = Api(); 17 | 18 | bool loadingStatus = false; 19 | 20 | bool joinButtonClicked = false; 21 | 22 | bool error = false; 23 | String? errorMessage; 24 | 25 | bool verifyParameters(String meetingId, String attendeeName) { 26 | if (meetingId.isEmpty || attendeeName.isEmpty) { 27 | _createError(Response.empty_parameter); 28 | return false; 29 | } else if (meetingId.length < 2 || meetingId.length > 64 || attendeeName.length < 2 || attendeeName.length > 64) { 30 | _createError(Response.invalid_attendee_or_meeting); 31 | return false; 32 | } 33 | return true; 34 | } 35 | 36 | Future joinMeeting(MeetingViewModel meetingProvider, MethodChannelCoordinator methodChannelProvider, String meetingId, 37 | String attendeeName) async { 38 | logger.i("Joining Meeting..."); 39 | _resetError(); 40 | 41 | bool audioPermissions = await _requestAudioPermissions(methodChannelProvider); 42 | bool videoPermissions = await _requestVideoPermissions(methodChannelProvider); 43 | 44 | // Create error messages for incorrect permissions 45 | if (!_checkPermissions(audioPermissions, videoPermissions)) { 46 | return false; 47 | } 48 | 49 | // Check if device is connected to the internet 50 | bool deviceIsConnected = await _isConnectedToInternet(); 51 | if (!deviceIsConnected) { 52 | _createError(Response.not_connected_to_internet); 53 | return false; 54 | } 55 | 56 | // Make call to api and recieve info in ApiResponse format 57 | final ApiResponse? apiResponse = await api.join(meetingId, attendeeName); 58 | 59 | // Check if ApiResponse is not null or returns a false response value indicating failed api call 60 | if (apiResponse == null) { 61 | _createError(Response.api_response_null); 62 | return false; 63 | } else if (!apiResponse.response) { 64 | if (apiResponse.error != null) { 65 | _createError(apiResponse.error!); 66 | return false; 67 | } 68 | } 69 | 70 | // Set meeetingData in meetingProvider 71 | if (apiResponse.response && apiResponse.content != null) { 72 | meetingProvider.intializeMeetingData(apiResponse.content!); 73 | } 74 | 75 | // Convert JoinInfo object to JSON 76 | if (meetingProvider.meetingData == null) { 77 | _createError(Response.null_meeting_data); 78 | return false; 79 | } 80 | final Map jsonArgsToSend = api.joinInfoToJSON(meetingProvider.meetingData!); 81 | 82 | // Send JSON to iOS 83 | MethodChannelResponse? joinResponse = await methodChannelProvider.callMethod(MethodCallOption.join, jsonArgsToSend); 84 | 85 | if (joinResponse == null) { 86 | _createError(Response.null_join_response); 87 | return false; 88 | } 89 | 90 | if (joinResponse.result) { 91 | logger.d(joinResponse.arguments); 92 | _toggleLoadingStatus(startLoading: false); 93 | meetingProvider.initializeLocalAttendee(); 94 | await meetingProvider.listAudioDevices(); 95 | await meetingProvider.initialAudioSelection(); 96 | return true; 97 | } else { 98 | _createError(joinResponse.arguments); 99 | return false; 100 | } 101 | } 102 | 103 | Future _requestAudioPermissions(MethodChannelCoordinator methodChannelProvider) async { 104 | MethodChannelResponse? audioPermission = await methodChannelProvider.callMethod(MethodCallOption.manageAudioPermissions); 105 | if (audioPermission == null) { 106 | return false; 107 | } 108 | if (audioPermission.result) { 109 | logger.i(audioPermission.arguments); 110 | } else { 111 | logger.e(audioPermission.arguments); 112 | } 113 | return audioPermission.result; 114 | } 115 | 116 | Future _requestVideoPermissions(MethodChannelCoordinator methodChannelProvider) async { 117 | MethodChannelResponse? videoPermission = await methodChannelProvider.callMethod(MethodCallOption.manageVideoPermissions); 118 | if (videoPermission != null) { 119 | if (videoPermission.result) { 120 | logger.i(videoPermission.arguments); 121 | } else { 122 | logger.e(videoPermission.arguments); 123 | } 124 | return videoPermission.result; 125 | } 126 | return false; 127 | } 128 | 129 | bool _checkPermissions(bool audioPermissions, bool videoPermissions) { 130 | if (!audioPermissions && !videoPermissions) { 131 | _createError(Response.audio_and_video_permission_denied); 132 | return false; 133 | } else if (!audioPermissions) { 134 | _createError(Response.audio_not_authorized); 135 | return false; 136 | } else if (!videoPermissions) { 137 | _createError(Response.video_not_authorized); 138 | return false; 139 | } 140 | return true; 141 | } 142 | 143 | void _createError(String errorMessage) { 144 | error = true; 145 | this.errorMessage = errorMessage; 146 | logger.e(errorMessage); 147 | _toggleLoadingStatus(startLoading: false); 148 | notifyListeners(); 149 | } 150 | 151 | void _resetError() { 152 | _toggleLoadingStatus(startLoading: true); 153 | error = false; 154 | errorMessage = null; 155 | notifyListeners(); 156 | } 157 | 158 | void _toggleLoadingStatus({required bool startLoading}) { 159 | loadingStatus = startLoading; 160 | notifyListeners(); 161 | } 162 | 163 | Future _isConnectedToInternet() async { 164 | try { 165 | final result = await InternetAddress.lookup('example.com'); 166 | if (result.isNotEmpty && result[0].rawAddress.isNotEmpty) { 167 | return true; 168 | } 169 | } on SocketException catch (_) { 170 | return false; 171 | } 172 | return false; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lib/view_models/meeting_view_model.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_demo_chime_sdk/interfaces/audio_devices_interface.dart'; 8 | import 'package:flutter_demo_chime_sdk/interfaces/audio_video_interface.dart'; 9 | import 'package:flutter_demo_chime_sdk/interfaces/video_tile_interface.dart'; 10 | import 'package:flutter_demo_chime_sdk/response_enums.dart'; 11 | import 'package:flutter_demo_chime_sdk/interfaces/realtime_interface.dart'; 12 | import 'package:provider/provider.dart'; 13 | import '../attendee.dart'; 14 | import '../logger.dart'; 15 | 16 | import '../api.dart'; 17 | import '../method_channel_coordinator.dart'; 18 | import '../video_tile.dart'; 19 | 20 | class MeetingViewModel extends ChangeNotifier 21 | implements RealtimeInterface, VideoTileInterface, AudioDevicesInterface, AudioVideoInterface { 22 | String? meetingId; 23 | 24 | JoinInfo? meetingData; 25 | 26 | MethodChannelCoordinator? methodChannelProvider; 27 | 28 | String? localAttendeeId; 29 | String? remoteAttendeeId; 30 | String? contentAttendeeId; 31 | 32 | String? selectedAudioDevice; 33 | List deviceList = []; 34 | 35 | // AttendeeId is the key 36 | Map currAttendees = {}; 37 | 38 | bool isReceivingScreenShare = false; 39 | bool isMeetingActive = false; 40 | 41 | MeetingViewModel(BuildContext context) { 42 | methodChannelProvider = Provider.of(context, listen: false); 43 | } 44 | 45 | // 46 | // ————————————————————————— Initializers ————————————————————————— 47 | // 48 | 49 | void intializeMeetingData(JoinInfo meetData) { 50 | isMeetingActive = true; 51 | meetingData = meetData; 52 | meetingId = meetData.meeting.externalMeetingId; 53 | notifyListeners(); 54 | } 55 | 56 | void initializeLocalAttendee() { 57 | if (meetingData == null) { 58 | logger.e(Response.null_meeting_data); 59 | return; 60 | } 61 | localAttendeeId = meetingData!.attendee.attendeeId; 62 | 63 | if (localAttendeeId == null) { 64 | logger.e(Response.null_local_attendee); 65 | return; 66 | } 67 | currAttendees[localAttendeeId!] = Attendee(localAttendeeId!, meetingData!.attendee.externalUserId); 68 | notifyListeners(); 69 | } 70 | 71 | // 72 | // ————————————————————————— Interface Methods ————————————————————————— 73 | // 74 | 75 | @override 76 | void attendeeDidJoin(Attendee attendee) { 77 | String? attendeeIdToAdd = attendee.attendeeId; 78 | if (_isAttendeeContent(attendeeIdToAdd)) { 79 | logger.i("Content detected"); 80 | contentAttendeeId = attendeeIdToAdd; 81 | if (contentAttendeeId != null) { 82 | currAttendees[contentAttendeeId!] = attendee; 83 | logger.i("Content added to the meeting"); 84 | } 85 | notifyListeners(); 86 | return; 87 | } 88 | 89 | if (attendeeIdToAdd != localAttendeeId) { 90 | remoteAttendeeId = attendeeIdToAdd; 91 | if (remoteAttendeeId == null) { 92 | logger.e(Response.null_remote_attendee); 93 | return; 94 | } 95 | currAttendees[remoteAttendeeId!] = attendee; 96 | logger.i("${formatExternalUserId(currAttendees[remoteAttendeeId]?.externalUserId)} has joined the meeting."); 97 | notifyListeners(); 98 | } 99 | } 100 | 101 | // Used for both leave and drop callbacks 102 | @override 103 | void attendeeDidLeave(Attendee attendee, {required bool didDrop}) { 104 | final attIdToDelete = attendee.attendeeId; 105 | currAttendees.remove(attIdToDelete); 106 | if (didDrop) { 107 | logger.i("${formatExternalUserId(attendee.externalUserId)} has dropped from the meeting"); 108 | } else { 109 | logger.i("${formatExternalUserId(attendee.externalUserId)} has left the meeting"); 110 | } 111 | notifyListeners(); 112 | } 113 | 114 | @override 115 | void attendeeDidMute(Attendee attendee) { 116 | _changeMuteStatus(attendee, mute: true); 117 | } 118 | 119 | @override 120 | void attendeeDidUnmute(Attendee attendee) { 121 | _changeMuteStatus(attendee, mute: false); 122 | } 123 | 124 | @override 125 | void videoTileDidAdd(String attendeeId, VideoTile videoTile) { 126 | currAttendees[attendeeId]?.videoTile = videoTile; 127 | if (videoTile.isContentShare) { 128 | isReceivingScreenShare = true; 129 | notifyListeners(); 130 | return; 131 | } 132 | currAttendees[attendeeId]?.isVideoOn = true; 133 | notifyListeners(); 134 | } 135 | 136 | @override 137 | void videoTileDidRemove(String attendeeId, VideoTile videoTile) { 138 | if (videoTile.isContentShare) { 139 | currAttendees[contentAttendeeId]?.videoTile = null; 140 | isReceivingScreenShare = false; 141 | } else { 142 | currAttendees[attendeeId]?.videoTile = null; 143 | currAttendees[attendeeId]?.isVideoOn = false; 144 | } 145 | notifyListeners(); 146 | } 147 | 148 | @override 149 | Future initialAudioSelection() async { 150 | MethodChannelResponse? device = await methodChannelProvider?.callMethod(MethodCallOption.initialAudioSelection); 151 | if (device == null) { 152 | logger.e(Response.null_initial_audio_device); 153 | return; 154 | } 155 | logger.i("Initial audio device selection: ${device.arguments}"); 156 | selectedAudioDevice = device.arguments; 157 | notifyListeners(); 158 | } 159 | 160 | @override 161 | Future listAudioDevices() async { 162 | MethodChannelResponse? devices = await methodChannelProvider?.callMethod(MethodCallOption.listAudioDevices); 163 | 164 | if (devices == null) { 165 | logger.e(Response.null_audio_device_list); 166 | return; 167 | } 168 | final deviceIterable = devices.arguments.map((device) => device.toString()); 169 | 170 | final devList = List.from(deviceIterable.toList()); 171 | logger.d("Devices available: $devList"); 172 | deviceList = devList; 173 | notifyListeners(); 174 | } 175 | 176 | @override 177 | void updateCurrentDevice(String device) async { 178 | MethodChannelResponse? updateDeviceResponse = 179 | await methodChannelProvider?.callMethod(MethodCallOption.updateAudioDevice, device); 180 | 181 | if (updateDeviceResponse == null) { 182 | logger.e(Response.null_audio_device_update); 183 | return; 184 | } 185 | 186 | if (updateDeviceResponse.result) { 187 | logger.i("${updateDeviceResponse.arguments} to: $device"); 188 | selectedAudioDevice = device; 189 | notifyListeners(); 190 | } else { 191 | logger.e("${updateDeviceResponse.arguments}"); 192 | } 193 | } 194 | 195 | @override 196 | void audioSessionDidStop() { 197 | logger.i("Audio session stopped by AudioVideoObserver."); 198 | _resetMeetingValues(); 199 | } 200 | 201 | // 202 | // —————————————————————————— Methods —————————————————————————————————————— 203 | // 204 | 205 | void _changeMuteStatus(Attendee attendee, {required bool mute}) { 206 | final attIdToggleMute = attendee.attendeeId; 207 | currAttendees[attIdToggleMute]?.muteStatus = mute; 208 | if (mute) { 209 | logger.i("${formatExternalUserId(attendee.externalUserId)} has been muted"); 210 | } else { 211 | logger.i("${formatExternalUserId(attendee.externalUserId)} has been unmuted"); 212 | } 213 | notifyListeners(); 214 | } 215 | 216 | void sendLocalMuteToggle() async { 217 | if (!currAttendees.containsKey(localAttendeeId)) { 218 | logger.e("Local attendee not found"); 219 | return; 220 | } 221 | 222 | if (currAttendees[localAttendeeId]!.muteStatus) { 223 | MethodChannelResponse? unmuteResponse = await methodChannelProvider?.callMethod(MethodCallOption.unmute); 224 | if (unmuteResponse == null) { 225 | logger.e(Response.unmute_response_null); 226 | return; 227 | } 228 | 229 | if (unmuteResponse.result) { 230 | logger.i("${unmuteResponse.arguments} ${formatExternalUserId(currAttendees[localAttendeeId]?.externalUserId)}"); 231 | notifyListeners(); 232 | } else { 233 | logger.e("${unmuteResponse.arguments} ${formatExternalUserId(currAttendees[localAttendeeId]?.externalUserId)}"); 234 | } 235 | } else { 236 | MethodChannelResponse? muteResponse = await methodChannelProvider?.callMethod(MethodCallOption.mute); 237 | if (muteResponse == null) { 238 | logger.e(Response.mute_response_null); 239 | return; 240 | } 241 | 242 | if (muteResponse.result) { 243 | logger.i("${muteResponse.arguments} ${formatExternalUserId(currAttendees[localAttendeeId]?.externalUserId)}"); 244 | notifyListeners(); 245 | } else { 246 | logger.e("${muteResponse.arguments} ${formatExternalUserId(currAttendees[localAttendeeId]?.externalUserId)}"); 247 | } 248 | } 249 | } 250 | 251 | void sendLocalVideoTileOn() async { 252 | if (!currAttendees.containsKey(localAttendeeId)) { 253 | logger.e("Local attendee not found"); 254 | return; 255 | } 256 | 257 | if (currAttendees[localAttendeeId]!.isVideoOn) { 258 | MethodChannelResponse? videoStopped = await methodChannelProvider?.callMethod(MethodCallOption.localVideoOff); 259 | if (videoStopped == null) { 260 | logger.e(Response.video_stopped_response_null); 261 | return; 262 | } 263 | 264 | if (videoStopped.result) { 265 | logger.i(videoStopped.arguments); 266 | } else { 267 | logger.e(videoStopped.arguments); 268 | } 269 | } else { 270 | MethodChannelResponse? videoStart = await methodChannelProvider?.callMethod(MethodCallOption.localVideoOn); 271 | if (videoStart == null) { 272 | logger.e(Response.video_start_response_null); 273 | return; 274 | } 275 | 276 | if (videoStart.result) { 277 | logger.i(videoStart.arguments); 278 | } else { 279 | logger.e(videoStart.arguments); 280 | } 281 | } 282 | } 283 | 284 | void stopMeeting() async { 285 | MethodChannelResponse? stopResponse = await methodChannelProvider?.callMethod(MethodCallOption.stop); 286 | if (stopResponse == null) { 287 | logger.e(Response.stop_response_null); 288 | return; 289 | } 290 | logger.i(stopResponse.arguments); 291 | } 292 | 293 | // 294 | // —————————————————————————— Helpers —————————————————————————————————————— 295 | // 296 | 297 | void _resetMeetingValues() { 298 | meetingId = null; 299 | meetingData = null; 300 | localAttendeeId = null; 301 | remoteAttendeeId = null; 302 | contentAttendeeId = null; 303 | selectedAudioDevice = null; 304 | deviceList = []; 305 | currAttendees = {}; 306 | isReceivingScreenShare = false; 307 | isMeetingActive = false; 308 | logger.i("Meeting values reset"); 309 | notifyListeners(); 310 | } 311 | 312 | String formatExternalUserId(String? externalUserId) { 313 | List? externalUserIdArray = externalUserId?.split("#"); 314 | if (externalUserIdArray == null) { 315 | return "UNKNOWN"; 316 | } 317 | String extUserId = externalUserIdArray.length == 2 ? externalUserIdArray[1] : "UNKNOWN"; 318 | return extUserId; 319 | } 320 | 321 | bool _isAttendeeContent(String? attendeeId) { 322 | List? attendeeIdArray = attendeeId?.split("#"); 323 | return attendeeIdArray?.length == 2; 324 | } 325 | } 326 | -------------------------------------------------------------------------------- /lib/views/join_meeting.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import 'package:flutter/material.dart'; 7 | import 'package:flutter_demo_chime_sdk/method_channel_coordinator.dart'; 8 | import 'package:provider/provider.dart'; 9 | 10 | import '../view_models/join_meeting_view_model.dart'; 11 | import '../view_models/meeting_view_model.dart'; 12 | import 'meeting.dart'; 13 | 14 | class JoinMeetingView extends StatelessWidget { 15 | JoinMeetingView({Key? key}) : super(key: key); 16 | 17 | final TextEditingController meetingIdTEC = TextEditingController(); 18 | final TextEditingController attendeeIdTEC = TextEditingController(); 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | final joinMeetingProvider = Provider.of(context); 23 | final methodChannelProvider = Provider.of(context); 24 | final meetingProvider = Provider.of(context); 25 | 26 | final orientation = MediaQuery.of(context).orientation; 27 | 28 | return joinMeetingBody(joinMeetingProvider, methodChannelProvider, meetingProvider, context, orientation); 29 | } 30 | 31 | // 32 | // —————————————————————————— Main Body —————————————————————————————————————— 33 | // 34 | 35 | Widget joinMeetingBody(JoinMeetingViewModel joinMeetingProvider, MethodChannelCoordinator methodChannelProvider, 36 | MeetingViewModel meetingProvider, BuildContext context, Orientation orientation) { 37 | if (orientation == Orientation.portrait) { 38 | return joinMeetingBodyPortrait(joinMeetingProvider, methodChannelProvider, meetingProvider, context); 39 | } else { 40 | return joinMeetingBodyLandscape(joinMeetingProvider, methodChannelProvider, meetingProvider, context); 41 | } 42 | } 43 | 44 | // 45 | // —————————————————————————— Portrait Body —————————————————————————————————————— 46 | // 47 | 48 | Widget joinMeetingBodyPortrait(JoinMeetingViewModel joinMeetingProvider, MethodChannelCoordinator methodChannelProvider, 49 | MeetingViewModel meetingProvider, BuildContext context) { 50 | return Scaffold( 51 | appBar: AppBar( 52 | title: const Text('Amazon Chime SDK'), 53 | ), 54 | body: Center( 55 | child: Column( 56 | mainAxisAlignment: MainAxisAlignment.center, 57 | children: [ 58 | titleFlutterDemo(5), 59 | Padding( 60 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 10), 61 | child: meetingTextField(meetingIdTEC), 62 | ), 63 | Padding( 64 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), 65 | child: attendeeTextField(attendeeIdTEC), 66 | ), 67 | joinButton(joinMeetingProvider, methodChannelProvider, meetingProvider, context), 68 | loadingIcon(joinMeetingProvider), 69 | errorMessage(joinMeetingProvider), 70 | ], 71 | ), 72 | ), 73 | ); 74 | } 75 | 76 | // 77 | // —————————————————————————— Landscape Body —————————————————————————————————————— 78 | // 79 | 80 | Widget joinMeetingBodyLandscape(JoinMeetingViewModel joinMeetingProvider, MethodChannelCoordinator methodChannelProvider, 81 | MeetingViewModel meetingProvider, BuildContext context) { 82 | return Scaffold( 83 | body: SingleChildScrollView( 84 | child: Center( 85 | child: Column( 86 | mainAxisAlignment: MainAxisAlignment.center, 87 | children: [ 88 | const SizedBox( 89 | height: 60, 90 | ), 91 | titleFlutterDemo(10), 92 | Padding( 93 | padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 10), 94 | child: meetingTextField(meetingIdTEC), 95 | ), 96 | Padding( 97 | padding: const EdgeInsets.symmetric(horizontal: 60, vertical: 16), 98 | child: attendeeTextField(attendeeIdTEC), 99 | ), 100 | joinButton(joinMeetingProvider, methodChannelProvider, meetingProvider, context), 101 | loadingIcon(joinMeetingProvider), 102 | errorMessage(joinMeetingProvider), 103 | ], 104 | ), 105 | ), 106 | ), 107 | ); 108 | } 109 | 110 | // 111 | // —————————————————————————— Helpers —————————————————————————————————————— 112 | // 113 | 114 | Widget joinButton(JoinMeetingViewModel joinMeetingProvider, MethodChannelCoordinator methodChannelProvider, 115 | MeetingViewModel meetingProvider, BuildContext context) { 116 | return ElevatedButton( 117 | child: const Text("Join Meeting"), 118 | onPressed: () async { 119 | if (!joinMeetingProvider.joinButtonClicked) { 120 | // Prevent multiple clicks 121 | joinMeetingProvider.joinButtonClicked = true; 122 | 123 | // Hide Keyboard 124 | FocusManager.instance.primaryFocus?.unfocus(); 125 | 126 | String meeetingId = meetingIdTEC.text.trim(); 127 | String attendeeId = attendeeIdTEC.text.trim(); 128 | 129 | if (joinMeetingProvider.verifyParameters(meeetingId, attendeeId)) { 130 | // Observers should be initialized before MethodCallHandler 131 | methodChannelProvider.initializeObservers(meetingProvider); 132 | methodChannelProvider.initializeMethodCallHandler(); 133 | 134 | // Call api, format to JSON and send to native 135 | bool isMeetingJoined = 136 | await joinMeetingProvider.joinMeeting(meetingProvider, methodChannelProvider, meeetingId, attendeeId); 137 | if (isMeetingJoined) { 138 | // ignore: use_build_context_synchronously 139 | Navigator.push( 140 | context, 141 | MaterialPageRoute( 142 | builder: (context) => const MeetingView(), 143 | ), 144 | ); 145 | } 146 | } 147 | joinMeetingProvider.joinButtonClicked = false; 148 | } 149 | }, 150 | ); 151 | } 152 | 153 | Widget titleFlutterDemo(double pad) { 154 | return Padding( 155 | padding: EdgeInsets.symmetric(vertical: pad), 156 | child: const Text( 157 | "Flutter Demo", 158 | style: TextStyle( 159 | fontSize: 32, 160 | color: Colors.blue, 161 | ), 162 | ), 163 | ); 164 | } 165 | 166 | Widget meetingTextField(meetingIdTEC) { 167 | return TextField( 168 | controller: meetingIdTEC, 169 | decoration: const InputDecoration( 170 | labelText: "Meeting ID", 171 | border: OutlineInputBorder(), 172 | ), 173 | ); 174 | } 175 | 176 | Widget attendeeTextField(attendeeIdTEC) { 177 | return TextField( 178 | controller: attendeeIdTEC, 179 | decoration: const InputDecoration( 180 | labelText: "Attendee Name", 181 | border: OutlineInputBorder(), 182 | ), 183 | ); 184 | } 185 | 186 | Widget loadingIcon(JoinMeetingViewModel joinMeetingProvider) { 187 | if (joinMeetingProvider.loadingStatus) { 188 | return const Padding(padding: EdgeInsets.symmetric(vertical: 10), child: CircularProgressIndicator()); 189 | } else { 190 | return const SizedBox.shrink(); 191 | } 192 | } 193 | 194 | Widget errorMessage(JoinMeetingViewModel joinMeetingProvider) { 195 | if (joinMeetingProvider.error) { 196 | return SingleChildScrollView( 197 | scrollDirection: Axis.vertical, 198 | child: Center( 199 | child: Padding( 200 | padding: const EdgeInsets.all(15), 201 | child: Text( 202 | "${joinMeetingProvider.errorMessage}", 203 | style: const TextStyle(color: Colors.red), 204 | ), 205 | ), 206 | ), 207 | ); 208 | } else { 209 | return const SizedBox.shrink(); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /lib/views/meeting.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import 'dart:io' show Platform; 7 | import 'package:flutter/foundation.dart'; 8 | import 'package:flutter/gestures.dart'; 9 | import 'package:flutter/material.dart'; 10 | import 'package:flutter/rendering.dart'; 11 | import 'package:flutter/services.dart'; 12 | import 'package:flutter_demo_chime_sdk/view_models/meeting_view_model.dart'; 13 | import 'package:flutter_demo_chime_sdk/views/screenshare.dart'; 14 | import 'package:provider/provider.dart'; 15 | 16 | import '../logger.dart'; 17 | import 'style.dart'; 18 | 19 | class MeetingView extends StatelessWidget { 20 | const MeetingView({Key? key}) : super(key: key); 21 | 22 | @override 23 | Widget build(BuildContext context) { 24 | final meetingProvider = Provider.of(context); 25 | final orientation = MediaQuery.of(context).orientation; 26 | 27 | if (!meetingProvider.isMeetingActive) { 28 | Navigator.maybePop(context); 29 | } 30 | 31 | return Scaffold( 32 | appBar: AppBar( 33 | title: Text("${meetingProvider.meetingId}"), 34 | automaticallyImplyLeading: false, 35 | ), 36 | resizeToAvoidBottomInset: true, 37 | body: meetingBody(orientation, meetingProvider, context), 38 | ); 39 | } 40 | 41 | // 42 | // —————————————————————————— Main Body —————————————————————————————————————— 43 | // 44 | 45 | Widget meetingBody(Orientation orientation, MeetingViewModel meetingProvider, BuildContext context) { 46 | if (orientation == Orientation.portrait) { 47 | return meetingBodyPortrait(meetingProvider, orientation, context); 48 | } else { 49 | return meetingBodyLandscape(meetingProvider, orientation, context); 50 | } 51 | } 52 | 53 | // 54 | // —————————————————————————— Portrait Body —————————————————————————————————————— 55 | // 56 | 57 | Widget meetingBodyPortrait(MeetingViewModel meetingProvider, Orientation orientation, BuildContext context) { 58 | return Center( 59 | child: Column( 60 | children: [ 61 | const SizedBox( 62 | height: 8, 63 | ), 64 | Row( 65 | mainAxisAlignment: MainAxisAlignment.center, 66 | crossAxisAlignment: CrossAxisAlignment.center, 67 | children: displayVideoTiles(meetingProvider, orientation, context), 68 | ), 69 | const Padding( 70 | padding: EdgeInsets.only(top: 30, bottom: 20), 71 | child: Text( 72 | "Attendees", 73 | style: TextStyle(fontSize: Style.titleSize), 74 | textAlign: TextAlign.center, 75 | ), 76 | ), 77 | Column( 78 | children: displayAttendees(meetingProvider, context), 79 | ), 80 | WillPopScope( 81 | onWillPop: () async { 82 | meetingProvider.stopMeeting(); 83 | return true; 84 | }, 85 | child: const Spacer(), 86 | ), 87 | Padding( 88 | padding: const EdgeInsets.symmetric(vertical: 50), 89 | child: SizedBox( 90 | height: 50, 91 | width: 300, 92 | child: leaveMeetingButton(meetingProvider, context), 93 | ), 94 | ), 95 | ], 96 | ), 97 | ); 98 | } 99 | 100 | List displayAttendees(MeetingViewModel meetingProvider, BuildContext context) { 101 | List attendees = []; 102 | if (meetingProvider.currAttendees.containsKey(meetingProvider.localAttendeeId)) { 103 | attendees.add(localListInfo(meetingProvider, context)); 104 | } 105 | if (meetingProvider.currAttendees.length > 1) { 106 | if (meetingProvider.currAttendees.containsKey(meetingProvider.remoteAttendeeId)) { 107 | attendees.add(remoteListInfo(meetingProvider)); 108 | } 109 | } 110 | 111 | return attendees; 112 | } 113 | 114 | Widget localListInfo(MeetingViewModel meetingProvider, BuildContext context) { 115 | return ListTile( 116 | title: Text( 117 | meetingProvider.formatExternalUserId(meetingProvider.currAttendees[meetingProvider.localAttendeeId]?.externalUserId), 118 | style: const TextStyle( 119 | color: Colors.black, 120 | fontSize: Style.fontSize, 121 | ), 122 | ), 123 | trailing: Row( 124 | mainAxisSize: MainAxisSize.min, 125 | children: [ 126 | IconButton( 127 | icon: const Icon(Icons.headphones), 128 | iconSize: Style.iconSize, 129 | color: Colors.blue, 130 | onPressed: () { 131 | showAudioDeviceDialog(meetingProvider, context); 132 | }, 133 | ), 134 | IconButton( 135 | icon: Icon(localMuteIcon(meetingProvider)), 136 | iconSize: Style.iconSize, 137 | padding: EdgeInsets.symmetric(horizontal: Style.iconPadding), 138 | color: Colors.blue, 139 | onPressed: () { 140 | meetingProvider.sendLocalMuteToggle(); 141 | }, 142 | ), 143 | IconButton( 144 | icon: Icon(localVideoIcon(meetingProvider)), 145 | iconSize: Style.iconSize, 146 | padding: EdgeInsets.symmetric(horizontal: Style.iconPadding), 147 | constraints: const BoxConstraints(), 148 | color: Colors.blue, 149 | onPressed: () { 150 | meetingProvider.sendLocalVideoTileOn(); 151 | }, 152 | ), 153 | ], 154 | ), 155 | ); 156 | } 157 | 158 | Widget remoteListInfo(MeetingViewModel meetingProvider) { 159 | return (ListTile( 160 | trailing: Row( 161 | mainAxisSize: MainAxisSize.min, 162 | children: [ 163 | Padding( 164 | padding: EdgeInsets.symmetric(horizontal: Style.iconPadding), 165 | child: Icon( 166 | remoteMuteIcon(meetingProvider), 167 | size: Style.iconSize, 168 | ), 169 | ), 170 | Padding( 171 | padding: EdgeInsets.symmetric(horizontal: Style.iconPadding), 172 | child: Icon( 173 | remoteVideoIcon(meetingProvider), 174 | size: Style.iconSize, 175 | ), 176 | ), 177 | ], 178 | ), 179 | title: Text( 180 | meetingProvider.formatExternalUserId(meetingProvider.currAttendees[meetingProvider.remoteAttendeeId]?.externalUserId), 181 | style: const TextStyle(fontSize: Style.fontSize), 182 | ), 183 | )); 184 | } 185 | 186 | // 187 | // —————————————————————————— Landscape Body —————————————————————————————————————— 188 | // 189 | 190 | Widget meetingBodyLandscape(MeetingViewModel meetingProvider, Orientation orientation, BuildContext context) { 191 | return Row( 192 | mainAxisSize: MainAxisSize.max, 193 | mainAxisAlignment: MainAxisAlignment.center, 194 | children: [ 195 | Expanded( 196 | child: Row( 197 | mainAxisAlignment: MainAxisAlignment.start, 198 | mainAxisSize: MainAxisSize.max, 199 | children: displayVideoTiles(meetingProvider, orientation, context), 200 | ), 201 | ), 202 | Expanded( 203 | child: Column( 204 | mainAxisSize: MainAxisSize.max, 205 | mainAxisAlignment: MainAxisAlignment.start, 206 | crossAxisAlignment: CrossAxisAlignment.center, 207 | children: [ 208 | const SizedBox( 209 | height: 20, 210 | ), 211 | const Text( 212 | "Attendees", 213 | style: TextStyle(fontSize: Style.titleSize), 214 | ), 215 | Column( 216 | children: displayAttendeesLanscape(meetingProvider, context), 217 | ), 218 | WillPopScope( 219 | onWillPop: () async { 220 | meetingProvider.stopMeeting(); 221 | return true; 222 | }, 223 | child: const Spacer(), 224 | ), 225 | leaveMeetingButton(meetingProvider, context), 226 | const SizedBox( 227 | height: 20, 228 | ), 229 | ], 230 | ), 231 | ), 232 | ], 233 | ); 234 | } 235 | 236 | List displayAttendeesLanscape(MeetingViewModel meetingProvider, BuildContext context) { 237 | List attendees = []; 238 | if (meetingProvider.currAttendees.containsKey(meetingProvider.localAttendeeId)) { 239 | attendees.add(localListInfoLandscape(meetingProvider, context)); 240 | } 241 | if (meetingProvider.currAttendees.length > 1) { 242 | if (meetingProvider.currAttendees.containsKey(meetingProvider.remoteAttendeeId)) { 243 | attendees.add(remoteListInfoLandscape(meetingProvider)); 244 | } 245 | } 246 | 247 | return attendees; 248 | } 249 | 250 | Widget localListInfoLandscape(MeetingViewModel meetingProvider, BuildContext context) { 251 | return SizedBox( 252 | width: 500, 253 | child: ListTile( 254 | title: Text( 255 | meetingProvider.formatExternalUserId(meetingProvider.currAttendees[meetingProvider.localAttendeeId]?.externalUserId), 256 | style: const TextStyle( 257 | color: Colors.black, 258 | fontSize: Style.fontSize, 259 | ), 260 | ), 261 | trailing: Row( 262 | mainAxisSize: MainAxisSize.min, 263 | children: [ 264 | IconButton( 265 | icon: const Icon(Icons.headphones), 266 | iconSize: Style.iconSize, 267 | color: Colors.blue, 268 | onPressed: () { 269 | showAudioDeviceDialog(meetingProvider, context); 270 | }, 271 | ), 272 | IconButton( 273 | icon: Icon(localMuteIcon(meetingProvider)), 274 | iconSize: Style.iconSize, 275 | padding: EdgeInsets.symmetric(horizontal: Style.iconPadding), 276 | color: Colors.blue, 277 | onPressed: () { 278 | meetingProvider.sendLocalMuteToggle(); 279 | }, 280 | ), 281 | IconButton( 282 | icon: Icon(localVideoIcon(meetingProvider)), 283 | iconSize: Style.iconSize, 284 | padding: EdgeInsets.symmetric(horizontal: Style.iconPadding), 285 | constraints: const BoxConstraints(), 286 | color: Colors.blue, 287 | onPressed: () { 288 | meetingProvider.sendLocalVideoTileOn(); 289 | }, 290 | ), 291 | ], 292 | ), 293 | ), 294 | ); 295 | } 296 | 297 | Widget remoteListInfoLandscape(MeetingViewModel meetingProvider) { 298 | return SizedBox( 299 | width: 500, 300 | child: ListTile( 301 | title: Text( 302 | meetingProvider.formatExternalUserId(meetingProvider.currAttendees[meetingProvider.remoteAttendeeId]?.externalUserId), 303 | style: const TextStyle( 304 | color: Colors.black, 305 | fontSize: Style.fontSize, 306 | ), 307 | ), 308 | trailing: Row( 309 | mainAxisSize: MainAxisSize.min, 310 | children: [ 311 | Padding( 312 | padding: EdgeInsets.symmetric(horizontal: Style.iconPadding), 313 | child: Icon( 314 | remoteMuteIcon(meetingProvider), 315 | size: Style.iconSize, 316 | ), 317 | ), 318 | Padding( 319 | padding: EdgeInsets.symmetric(horizontal: Style.iconPadding), 320 | child: Icon( 321 | remoteVideoIcon(meetingProvider), 322 | size: Style.iconSize, 323 | ), 324 | ), 325 | ], 326 | ), 327 | ), 328 | ); 329 | } 330 | 331 | // 332 | // —————————————————————————— Helpers —————————————————————————————————————— 333 | // 334 | 335 | void openFullscreenDialog(BuildContext context, int? params, MeetingViewModel meetingProvider) { 336 | Widget contentTile; 337 | 338 | if (Platform.isIOS) { 339 | contentTile = UiKitView( 340 | viewType: "videoTile", 341 | creationParams: params, 342 | creationParamsCodec: const StandardMessageCodec(), 343 | ); 344 | } else if (Platform.isAndroid) { 345 | contentTile = PlatformViewLink( 346 | viewType: 'videoTile', 347 | surfaceFactory: (BuildContext context, PlatformViewController controller) { 348 | return AndroidViewSurface( 349 | controller: controller as AndroidViewController, 350 | gestureRecognizers: const >{}, 351 | hitTestBehavior: PlatformViewHitTestBehavior.opaque, 352 | ); 353 | }, 354 | onCreatePlatformView: (PlatformViewCreationParams params) { 355 | final AndroidViewController controller = PlatformViewsService.initExpensiveAndroidView( 356 | id: params.id, 357 | viewType: 'videoTile', 358 | layoutDirection: TextDirection.ltr, 359 | creationParams: params, 360 | creationParamsCodec: const StandardMessageCodec(), 361 | onFocus: () => params.onFocusChanged, 362 | ); 363 | controller.addOnPlatformViewCreatedListener(params.onPlatformViewCreated); 364 | controller.create(); 365 | return controller; 366 | }, 367 | ); 368 | } else { 369 | contentTile = const Text("Unrecognized Platform."); 370 | } 371 | 372 | if (!meetingProvider.isReceivingScreenShare) { 373 | Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const MeetingView())); 374 | } 375 | 376 | Navigator.pushReplacement( 377 | context, 378 | MaterialPageRoute( 379 | builder: (BuildContext context) { 380 | return Scaffold( 381 | body: Column( 382 | children: [ 383 | Expanded( 384 | child: SizedBox( 385 | width: double.infinity, 386 | child: GestureDetector( 387 | onDoubleTap: () => 388 | Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const MeetingView())), 389 | child: contentTile), 390 | ), 391 | ), 392 | ], 393 | ), 394 | ); 395 | }, 396 | fullscreenDialog: true, 397 | ), 398 | ); 399 | } 400 | 401 | List displayVideoTiles(MeetingViewModel meetingProvider, Orientation orientation, BuildContext context) { 402 | Widget screenShareWidget = Expanded(child: videoTile(meetingProvider, context, isLocal: false, isContent: true)); 403 | Widget localVideoTile = videoTile(meetingProvider, context, isLocal: true, isContent: false); 404 | Widget remoteVideoTile = videoTile(meetingProvider, context, isLocal: false, isContent: false); 405 | 406 | if (meetingProvider.currAttendees.containsKey(meetingProvider.contentAttendeeId)) { 407 | if (meetingProvider.isReceivingScreenShare) { 408 | return [screenShareWidget]; 409 | } 410 | } 411 | 412 | List videoTiles = []; 413 | 414 | if (meetingProvider.currAttendees[meetingProvider.localAttendeeId]?.isVideoOn ?? false) { 415 | if (meetingProvider.currAttendees[meetingProvider.localAttendeeId]?.videoTile != null) { 416 | videoTiles.add(localVideoTile); 417 | } 418 | } 419 | if (meetingProvider.currAttendees.length > 1) { 420 | if (meetingProvider.currAttendees.containsKey(meetingProvider.remoteAttendeeId)) { 421 | if ((meetingProvider.currAttendees[meetingProvider.remoteAttendeeId]?.isVideoOn ?? false) && 422 | meetingProvider.currAttendees[meetingProvider.remoteAttendeeId]?.videoTile != null) { 423 | videoTiles.add(Expanded(child: remoteVideoTile)); 424 | } 425 | } 426 | } 427 | 428 | if (videoTiles.isEmpty) { 429 | const Widget emptyVideos = Text("No video detected"); 430 | if (orientation == Orientation.portrait) { 431 | videoTiles.add( 432 | emptyVideos, 433 | ); 434 | } else { 435 | videoTiles.add( 436 | const Center( 437 | widthFactor: 2.5, 438 | child: emptyVideos, 439 | ), 440 | ); 441 | } 442 | } 443 | 444 | return videoTiles; 445 | } 446 | 447 | Widget contentVideoTile(int? paramsVT, MeetingViewModel meetingProvider, BuildContext context) { 448 | Widget videoTile; 449 | if (Platform.isIOS) { 450 | videoTile = UiKitView( 451 | viewType: "videoTile", 452 | creationParams: paramsVT, 453 | creationParamsCodec: const StandardMessageCodec(), 454 | ); 455 | } else if (Platform.isAndroid) { 456 | videoTile = PlatformViewLink( 457 | viewType: 'videoTile', 458 | surfaceFactory: (BuildContext context, PlatformViewController controller) { 459 | return AndroidViewSurface( 460 | controller: controller as AndroidViewController, 461 | gestureRecognizers: const >{}, 462 | hitTestBehavior: PlatformViewHitTestBehavior.opaque, 463 | ); 464 | }, 465 | onCreatePlatformView: (PlatformViewCreationParams params) { 466 | final AndroidViewController controller = PlatformViewsService.initExpensiveAndroidView( 467 | id: params.id, 468 | viewType: 'videoTile', 469 | layoutDirection: TextDirection.ltr, 470 | creationParams: paramsVT, 471 | creationParamsCodec: const StandardMessageCodec(), 472 | onFocus: () => params.onFocusChanged, 473 | ); 474 | controller.addOnPlatformViewCreatedListener(params.onPlatformViewCreated); 475 | controller.create(); 476 | return controller; 477 | }, 478 | ); 479 | } else { 480 | videoTile = const Text("Unrecognized Platform."); 481 | } 482 | 483 | return Padding( 484 | padding: const EdgeInsets.symmetric(horizontal: 4), 485 | child: SizedBox( 486 | width: 200, 487 | height: 230, 488 | child: GestureDetector( 489 | onDoubleTap: () { 490 | Navigator.push(context, MaterialPageRoute(builder: (context) => ScreenShare(paramsVT: paramsVT))); 491 | }, 492 | child: videoTile, 493 | ), 494 | ), 495 | ); 496 | } 497 | 498 | Widget videoTile(MeetingViewModel meetingProvider, BuildContext context, {required bool isLocal, required bool isContent}) { 499 | int? paramsVT; 500 | 501 | if (isContent) { 502 | if (meetingProvider.contentAttendeeId != null) { 503 | if (meetingProvider.currAttendees[meetingProvider.contentAttendeeId]?.videoTile != null) { 504 | paramsVT = meetingProvider.currAttendees[meetingProvider.contentAttendeeId]?.videoTile?.tileId as int; 505 | return contentVideoTile(paramsVT, meetingProvider, context); 506 | } 507 | } 508 | } else if (isLocal) { 509 | paramsVT = meetingProvider.currAttendees[meetingProvider.localAttendeeId]?.videoTile?.tileId; 510 | } else { 511 | paramsVT = meetingProvider.currAttendees[meetingProvider.remoteAttendeeId]?.videoTile?.tileId; 512 | } 513 | 514 | Widget videoTile; 515 | if (Platform.isIOS) { 516 | videoTile = UiKitView( 517 | viewType: "videoTile", 518 | creationParams: paramsVT, 519 | creationParamsCodec: const StandardMessageCodec(), 520 | ); 521 | } else if (Platform.isAndroid) { 522 | videoTile = PlatformViewLink( 523 | viewType: 'videoTile', 524 | surfaceFactory: (BuildContext context, PlatformViewController controller) { 525 | return AndroidViewSurface( 526 | controller: controller as AndroidViewController, 527 | gestureRecognizers: const >{}, 528 | hitTestBehavior: PlatformViewHitTestBehavior.opaque, 529 | ); 530 | }, 531 | onCreatePlatformView: (PlatformViewCreationParams params) { 532 | final AndroidViewController controller = PlatformViewsService.initExpensiveAndroidView( 533 | id: params.id, 534 | viewType: 'videoTile', 535 | layoutDirection: TextDirection.ltr, 536 | creationParams: paramsVT, 537 | creationParamsCodec: const StandardMessageCodec(), 538 | ); 539 | controller.addOnPlatformViewCreatedListener(params.onPlatformViewCreated); 540 | controller.create(); 541 | return controller; 542 | }, 543 | ); 544 | } else { 545 | videoTile = const Text("Unrecognized Platform."); 546 | } 547 | 548 | return Padding( 549 | padding: const EdgeInsets.symmetric(horizontal: 4), 550 | child: SizedBox( 551 | width: 200, 552 | height: 230, 553 | child: videoTile, 554 | ), 555 | ); 556 | } 557 | 558 | void showAudioDeviceDialog(MeetingViewModel meetingProvider, BuildContext context) async { 559 | String? device = await showDialog( 560 | context: context, 561 | builder: (context) { 562 | return SimpleDialog( 563 | title: const Text("Choose Audio Device"), 564 | elevation: 40, 565 | titleTextStyle: const TextStyle(color: Colors.black, fontSize: Style.fontSize, fontWeight: FontWeight.bold), 566 | backgroundColor: Colors.white, 567 | children: getSimpleDialogOptionsAudioDevices(meetingProvider, context), 568 | ); 569 | }); 570 | if (device == null) { 571 | logger.w("No device chosen."); 572 | return; 573 | } 574 | 575 | meetingProvider.updateCurrentDevice(device); 576 | } 577 | 578 | List getSimpleDialogOptionsAudioDevices(MeetingViewModel meetingProvider, BuildContext context) { 579 | List dialogOptions = []; 580 | FontWeight weight; 581 | for (var i = 0; i < meetingProvider.deviceList.length; i++) { 582 | if (meetingProvider.deviceList[i] == meetingProvider.selectedAudioDevice) { 583 | weight = FontWeight.bold; 584 | } else { 585 | weight = FontWeight.normal; 586 | } 587 | dialogOptions.add( 588 | SimpleDialogOption( 589 | child: Text( 590 | meetingProvider.deviceList[i] as String, 591 | style: TextStyle(color: Colors.black, fontWeight: weight), 592 | ), 593 | onPressed: () { 594 | logger.i("${meetingProvider.deviceList[i]} was chosen."); 595 | Navigator.pop(context, meetingProvider.deviceList[i]); 596 | }, 597 | ), 598 | ); 599 | } 600 | return dialogOptions; 601 | } 602 | 603 | Widget leaveMeetingButton(MeetingViewModel meetingProvider, BuildContext context) { 604 | return ElevatedButton( 605 | style: ElevatedButton.styleFrom(primary: Colors.red), 606 | onPressed: () { 607 | meetingProvider.stopMeeting(); 608 | Navigator.pop(context); 609 | }, 610 | child: const Text("Leave Meeting"), 611 | ); 612 | } 613 | 614 | IconData localMuteIcon(MeetingViewModel meetingProvider) { 615 | if (!meetingProvider.currAttendees[meetingProvider.localAttendeeId]!.muteStatus) { 616 | return Icons.mic; 617 | } else { 618 | return Icons.mic_off; 619 | } 620 | } 621 | 622 | IconData remoteMuteIcon(MeetingViewModel meetingProvider) { 623 | if (!meetingProvider.currAttendees[meetingProvider.remoteAttendeeId]!.muteStatus) { 624 | return Icons.mic; 625 | } else { 626 | return Icons.mic_off; 627 | } 628 | } 629 | 630 | IconData localVideoIcon(MeetingViewModel meetingProvider) { 631 | if (meetingProvider.currAttendees[meetingProvider.localAttendeeId]!.isVideoOn) { 632 | return Icons.videocam; 633 | } else { 634 | return Icons.videocam_off; 635 | } 636 | } 637 | 638 | IconData remoteVideoIcon(MeetingViewModel meetingProvider) { 639 | if (meetingProvider.currAttendees[meetingProvider.remoteAttendeeId]!.isVideoOn) { 640 | return Icons.videocam; 641 | } else { 642 | return Icons.videocam_off; 643 | } 644 | } 645 | } 646 | -------------------------------------------------------------------------------- /lib/views/screenshare.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | import 'dart:io'; 7 | 8 | import 'package:flutter/foundation.dart'; 9 | import 'package:flutter/gestures.dart'; 10 | import 'package:flutter/material.dart'; 11 | import 'package:flutter/rendering.dart'; 12 | import 'package:flutter/services.dart'; 13 | import 'package:flutter_demo_chime_sdk/view_models/meeting_view_model.dart'; 14 | import 'package:provider/provider.dart'; 15 | 16 | class ScreenShare extends StatelessWidget { 17 | final int? paramsVT; 18 | 19 | const ScreenShare({super.key, required this.paramsVT}); 20 | 21 | @override 22 | Widget build(BuildContext context) { 23 | MeetingViewModel meetingProvider = Provider.of(context); 24 | 25 | Widget contentTile; 26 | Widget body; 27 | 28 | if (Platform.isIOS) { 29 | contentTile = UiKitView( 30 | viewType: "videoTile", 31 | creationParams: paramsVT as int, 32 | creationParamsCodec: const StandardMessageCodec(), 33 | ); 34 | } else if (Platform.isAndroid) { 35 | return PlatformViewLink( 36 | viewType: 'videoTile', 37 | surfaceFactory: (BuildContext context, PlatformViewController controller) { 38 | return AndroidViewSurface( 39 | controller: controller as AndroidViewController, 40 | gestureRecognizers: const >{}, 41 | hitTestBehavior: PlatformViewHitTestBehavior.opaque, 42 | ); 43 | }, 44 | onCreatePlatformView: (PlatformViewCreationParams params) { 45 | final AndroidViewController controller = PlatformViewsService.initExpensiveAndroidView( 46 | id: params.id, 47 | viewType: 'videoTile', 48 | layoutDirection: TextDirection.ltr, 49 | creationParams: paramsVT, 50 | creationParamsCodec: const StandardMessageCodec(), 51 | onFocus: () => params.onFocusChanged, 52 | ); 53 | controller.addOnPlatformViewCreatedListener(params.onPlatformViewCreated); 54 | controller.create(); 55 | return controller; 56 | }, 57 | ); 58 | } else { 59 | contentTile = const Text("Unrecognized Platform."); 60 | } 61 | 62 | if (!meetingProvider.isReceivingScreenShare) { 63 | body = GestureDetector( 64 | onDoubleTap: () => Navigator.popAndPushNamed(context, "/meeting"), 65 | child: Column( 66 | mainAxisAlignment: MainAxisAlignment.center, 67 | children: const [ 68 | Center( 69 | child: Text("Screenshare is no longer active."), 70 | ), 71 | Center( 72 | child: Text( 73 | "Double tap to go back to meeting.", 74 | style: TextStyle(color: Colors.grey), 75 | ), 76 | ), 77 | ], 78 | ), 79 | ); 80 | } else { 81 | body = Column( 82 | children: [ 83 | Expanded( 84 | child: SizedBox( 85 | width: double.infinity, 86 | child: GestureDetector( 87 | onDoubleTap: () => Navigator.popAndPushNamed(context, "/meeting"), 88 | child: contentTile, 89 | ), 90 | ), 91 | ), 92 | ], 93 | ); 94 | } 95 | 96 | return WillPopScope( 97 | onWillPop: () async { 98 | Navigator.popAndPushNamed(context, "/meeting"); 99 | return false; 100 | }, 101 | child: Scaffold( 102 | body: body, 103 | ), 104 | ); 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /lib/views/style.dart: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | * SPDX-License-Identifier: MIT-0 4 | */ 5 | 6 | class Style { 7 | static const double titleSize = 34; 8 | static const double fontSize = 20; 9 | static double iconSize = 26; 10 | static double iconPadding = 15; 11 | } 12 | -------------------------------------------------------------------------------- /pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | url: "https://pub.dartlang.org" 9 | source: hosted 10 | version: "2.8.2" 11 | boolean_selector: 12 | dependency: transitive 13 | description: 14 | name: boolean_selector 15 | url: "https://pub.dartlang.org" 16 | source: hosted 17 | version: "2.1.0" 18 | characters: 19 | dependency: transitive 20 | description: 21 | name: characters 22 | url: "https://pub.dartlang.org" 23 | source: hosted 24 | version: "1.2.0" 25 | charcode: 26 | dependency: transitive 27 | description: 28 | name: charcode 29 | url: "https://pub.dartlang.org" 30 | source: hosted 31 | version: "1.3.1" 32 | clock: 33 | dependency: transitive 34 | description: 35 | name: clock 36 | url: "https://pub.dartlang.org" 37 | source: hosted 38 | version: "1.1.0" 39 | collection: 40 | dependency: transitive 41 | description: 42 | name: collection 43 | url: "https://pub.dartlang.org" 44 | source: hosted 45 | version: "1.16.0" 46 | cupertino_icons: 47 | dependency: "direct main" 48 | description: 49 | name: cupertino_icons 50 | url: "https://pub.dartlang.org" 51 | source: hosted 52 | version: "1.0.5" 53 | fake_async: 54 | dependency: transitive 55 | description: 56 | name: fake_async 57 | url: "https://pub.dartlang.org" 58 | source: hosted 59 | version: "1.3.0" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_lints: 66 | dependency: "direct dev" 67 | description: 68 | name: flutter_lints 69 | url: "https://pub.dartlang.org" 70 | source: hosted 71 | version: "2.0.1" 72 | flutter_test: 73 | dependency: "direct dev" 74 | description: flutter 75 | source: sdk 76 | version: "0.0.0" 77 | http: 78 | dependency: "direct main" 79 | description: 80 | name: http 81 | url: "https://pub.dartlang.org" 82 | source: hosted 83 | version: "0.13.4" 84 | http_parser: 85 | dependency: transitive 86 | description: 87 | name: http_parser 88 | url: "https://pub.dartlang.org" 89 | source: hosted 90 | version: "4.0.1" 91 | lints: 92 | dependency: transitive 93 | description: 94 | name: lints 95 | url: "https://pub.dartlang.org" 96 | source: hosted 97 | version: "2.0.0" 98 | logger: 99 | dependency: "direct main" 100 | description: 101 | name: logger 102 | url: "https://pub.dartlang.org" 103 | source: hosted 104 | version: "1.1.0" 105 | matcher: 106 | dependency: transitive 107 | description: 108 | name: matcher 109 | url: "https://pub.dartlang.org" 110 | source: hosted 111 | version: "0.12.11" 112 | material_color_utilities: 113 | dependency: transitive 114 | description: 115 | name: material_color_utilities 116 | url: "https://pub.dartlang.org" 117 | source: hosted 118 | version: "0.1.4" 119 | meta: 120 | dependency: transitive 121 | description: 122 | name: meta 123 | url: "https://pub.dartlang.org" 124 | source: hosted 125 | version: "1.7.0" 126 | nested: 127 | dependency: transitive 128 | description: 129 | name: nested 130 | url: "https://pub.dartlang.org" 131 | source: hosted 132 | version: "1.0.0" 133 | path: 134 | dependency: transitive 135 | description: 136 | name: path 137 | url: "https://pub.dartlang.org" 138 | source: hosted 139 | version: "1.8.1" 140 | provider: 141 | dependency: "direct main" 142 | description: 143 | name: provider 144 | url: "https://pub.dartlang.org" 145 | source: hosted 146 | version: "6.0.3" 147 | sky_engine: 148 | dependency: transitive 149 | description: flutter 150 | source: sdk 151 | version: "0.0.99" 152 | source_span: 153 | dependency: transitive 154 | description: 155 | name: source_span 156 | url: "https://pub.dartlang.org" 157 | source: hosted 158 | version: "1.8.2" 159 | stack_trace: 160 | dependency: transitive 161 | description: 162 | name: stack_trace 163 | url: "https://pub.dartlang.org" 164 | source: hosted 165 | version: "1.10.0" 166 | stream_channel: 167 | dependency: transitive 168 | description: 169 | name: stream_channel 170 | url: "https://pub.dartlang.org" 171 | source: hosted 172 | version: "2.1.0" 173 | string_scanner: 174 | dependency: transitive 175 | description: 176 | name: string_scanner 177 | url: "https://pub.dartlang.org" 178 | source: hosted 179 | version: "1.1.0" 180 | term_glyph: 181 | dependency: transitive 182 | description: 183 | name: term_glyph 184 | url: "https://pub.dartlang.org" 185 | source: hosted 186 | version: "1.2.0" 187 | test_api: 188 | dependency: transitive 189 | description: 190 | name: test_api 191 | url: "https://pub.dartlang.org" 192 | source: hosted 193 | version: "0.4.9" 194 | typed_data: 195 | dependency: transitive 196 | description: 197 | name: typed_data 198 | url: "https://pub.dartlang.org" 199 | source: hosted 200 | version: "1.3.1" 201 | vector_math: 202 | dependency: transitive 203 | description: 204 | name: vector_math 205 | url: "https://pub.dartlang.org" 206 | source: hosted 207 | version: "2.1.2" 208 | sdks: 209 | dart: ">=2.17.3 <3.0.0" 210 | flutter: ">=1.16.0" 211 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_demo_chime_sdk 2 | description: A new Flutter project. 3 | 4 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 5 | 6 | version: 1.0.0+1 7 | 8 | environment: 9 | sdk: ">=2.17.3 <3.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | cupertino_icons: ^1.0.2 15 | provider: ^6.0.3 16 | http: ^0.13.4 17 | logger: ^1.1.0 # If publishing to GitHub review this package 18 | 19 | dev_dependencies: 20 | flutter_test: 21 | sdk: flutter 22 | flutter_lints: ^2.0.0 23 | 24 | flutter: 25 | uses-material-design: true -------------------------------------------------------------------------------- /test/widget_test.dart: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------