├── app ├── .gitignore ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── strings.xml │ │ │ │ ├── colors.xml │ │ │ │ └── themes.xml │ │ │ ├── mipmap-hdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_video_switch.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-mdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_video_switch.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_video_switch.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_video_switch.png │ │ │ │ └── ic_launcher_round.png │ │ │ ├── mipmap-xxxhdpi │ │ │ │ ├── ic_launcher.png │ │ │ │ ├── ic_launcher_round.png │ │ │ │ └── ic_video_switch.png │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ ├── ic_launcher.xml │ │ │ │ └── ic_launcher_round.xml │ │ │ ├── layout │ │ │ │ └── activity_main.xml │ │ │ ├── values-night │ │ │ │ └── themes.xml │ │ │ └── drawable-v24 │ │ │ │ ├── ic_launcher_foreground.xml │ │ │ │ └── ic_launcher_background.xml │ │ ├── ic_launcher-playstore.png │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── io │ │ │ └── agora │ │ │ └── agora_android_uikit │ │ │ └── MainActivity.kt │ ├── test │ │ └── java │ │ │ └── io │ │ │ └── agora │ │ │ └── agora_android_uikit │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── io │ │ └── agora │ │ └── agora_android_uikit │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── .idea ├── .name ├── .gitignore ├── codeStyles │ ├── codeStyleConfig.xml │ └── Project.xml ├── compiler.xml ├── kotlinc.xml ├── codestream.xml ├── misc.xml └── jarRepositories.xml ├── agorauikit_android ├── .gitignore ├── consumer-rules.pro ├── src │ ├── main │ │ ├── res │ │ │ ├── values │ │ │ │ ├── dimens.xml │ │ │ │ └── strings.xml │ │ │ └── drawable │ │ │ │ ├── btn_switch_camera.png │ │ │ │ ├── button_background.xml │ │ │ │ ├── ic_person.xml │ │ │ │ ├── baseline_push_pin_20.xml │ │ │ │ ├── ic_round_pending_24.xml │ │ │ │ └── ic_video_mute.xml │ │ ├── AndroidManifest.xml │ │ └── java │ │ │ └── io │ │ │ └── agora │ │ │ └── agorauikit_android │ │ │ ├── utils.kt │ │ │ ├── AgoraConnectionData.kt │ │ │ ├── DevicePermissions.kt │ │ │ ├── AgoraRtmController │ │ │ ├── AgoraRtmClientHandler.kt │ │ │ ├── AgoraRtmChannelHandler.kt │ │ │ ├── AgoraRtmController+Tokens.kt │ │ │ ├── AgoraRtmController+Helpers.kt │ │ │ ├── AgoraRtmController.kt │ │ │ └── AgoraRtmController+MuteRequest.kt │ │ │ ├── AgoraButton.kt │ │ │ ├── AgoraVideoViewer+Token.kt │ │ │ ├── AgoraSingleVideoView.kt │ │ │ ├── AgoraSettings.kt │ │ │ ├── AgoraVideoViewer+Buttons.kt │ │ │ ├── AgoraVideoViewer+Ordering.kt │ │ │ ├── AgoraVideoViewer.kt │ │ │ └── AgoraVideoViewerHandler.kt │ ├── test │ │ └── java │ │ │ └── io │ │ │ └── agora │ │ │ └── agorauikit_android │ │ │ └── ExampleUnitTest.kt │ └── androidTest │ │ └── java │ │ └── io │ │ └── agora │ │ └── agorauikit_android │ │ └── ExampleInstrumentedTest.kt ├── proguard-rules.pro └── build.gradle.kts ├── settings.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── .github ├── workflows │ ├── kotlin-build.yml │ └── dokka-build.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── .vscode └── launch.json ├── LICENSE ├── gradle.properties ├── CONTRIBUTING.md ├── .gitignore ├── gradlew.bat ├── README.md ├── CODE_OF_CONDUCT.md └── gradlew /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /.idea/.name: -------------------------------------------------------------------------------- 1 | Agora-Android-UIKit -------------------------------------------------------------------------------- /agorauikit_android/.gitignore: -------------------------------------------------------------------------------- 1 | /build -------------------------------------------------------------------------------- /agorauikit_android/consumer-rules.pro: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include(":agorauikit_android") 2 | include(":app") 3 | rootProject.name = "Agora-Android-UIKit" 4 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | Agora-Android-UIKit 3 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_video_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-hdpi/ic_video_switch.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_video_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-mdpi/ic_video_switch.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /agorauikit_android/src/main/res/values/dimens.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 10dp 4 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-hdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-mdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-mdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_video_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-xhdpi/ic_video_switch.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_video_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_video_switch.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxxhdpi/ic_video_switch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/app/src/main/res/mipmap-xxxhdpi/ic_video_switch.png -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/res/drawable/btn_switch_camera.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AgoraIO-Community/VideoUIKit-Android/HEAD/agorauikit_android/src/main/res/drawable/btn_switch_camera.png -------------------------------------------------------------------------------- /agorauikit_android/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | AgoraVideoUIKit 6 | -------------------------------------------------------------------------------- /.idea/compiler.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/kotlinc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4.2-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/res/drawable/button_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/src/main/res/values/colors.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | #FFBB86FC 4 | #FF6200EE 5 | #FF3700B3 6 | #FF03DAC5 7 | #FF018786 8 | #FF000000 9 | #FFFFFFFF 10 | -------------------------------------------------------------------------------- /app/src/test/java/io/agora/agora_android_uikit/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agora_android_uikit 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /agorauikit_android/src/test/java/io/agora/agorauikit_android/ExampleUnitTest.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Test 5 | 6 | /** 7 | * Example local unit test, which will execute on the development machine (host). 8 | * 9 | * See [testing documentation](http://d.android.com/tools/testing). 10 | */ 11 | class ExampleUnitTest { 12 | @Test 13 | fun addition_isCorrect() { 14 | assertEquals(4, 2 + 2) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/res/drawable/ic_person.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/utils.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import android.content.Context 4 | import android.util.TypedValue 5 | 6 | /** 7 | * Converts DP unit to Px unit 8 | * 9 | * @param context Activity Context 10 | * @param DP The DP value 11 | * @return Px Value 12 | */ 13 | internal fun DPToPx(context: Context, DP: Int): Int { 14 | val res = context.resources 15 | return TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, DP.toFloat(), res.displayMetrics).toInt() 16 | } 17 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/res/drawable/baseline_push_pin_20.xml: -------------------------------------------------------------------------------- 1 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /.github/workflows/kotlin-build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | pull_request: 8 | branches: 9 | - "*" 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | with: 17 | fetch-depth: 0 18 | - name: Set up JDK 17 19 | uses: actions/setup-java@v3 20 | with: 21 | distribution: temurin 22 | java-version: 17 23 | - name: Setup Gradle 24 | uses: gradle/gradle-build-action@v2 25 | - name: ktlint 26 | run: ./gradlew ktlintCheck 27 | - name: Build with Gradle 28 | run: ./gradlew build publishToMavenLocal -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[FEATURE]" 5 | labels: enhancement 6 | assignees: Meherdeep 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/res/drawable/ic_round_pending_24.xml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraConnectionData.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | /** Storing struct for holding data about the connection to Agora service 4 | * Create AgoraConnectionData object 5 | * @param appId: Agora App ID from https://agora.io 6 | * @param appToken: Token to be used to connect to a channel, can be nil. 7 | */ 8 | data class AgoraConnectionData @JvmOverloads constructor( 9 | var appId: String, 10 | var appToken: String? = null, 11 | var username: String? = null, 12 | var rtmToken: String? = null, 13 | var rtmId: String? = null, 14 | var rtmChannelName: String? = null 15 | ) { 16 | /** 17 | * Channel the object is connected to. This cannot be set with the initialiser 18 | */ 19 | var channel: String? = null 20 | } 21 | -------------------------------------------------------------------------------- /app/src/androidTest/java/io/agora/agora_android_uikit/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agora_android_uikit 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("io.agora.agorauikit_android", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /agorauikit_android/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile -------------------------------------------------------------------------------- /agorauikit_android/src/androidTest/java/io/agora/agorauikit_android/ExampleInstrumentedTest.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import androidx.test.ext.junit.runners.AndroidJUnit4 4 | import androidx.test.platform.app.InstrumentationRegistry 5 | import org.junit.Assert.assertEquals 6 | import org.junit.Test 7 | import org.junit.runner.RunWith 8 | 9 | /** 10 | * Instrumented test, which will execute on an Android device. 11 | * 12 | * See [testing documentation](http://d.android.com/tools/testing). 13 | */ 14 | @RunWith(AndroidJUnit4::class) 15 | class ExampleInstrumentedTest { 16 | @Test 17 | fun useAppContext() { 18 | // Context of the app under test. 19 | val appContext = InstrumentationRegistry.getInstrumentation().targetContext 20 | assertEquals("io.agora.agorauikit_android.test", appContext.packageName) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # You can control the set of applied configuration files using the 3 | # proguardFiles setting in build.gradle.kts.kts. 4 | # 5 | # For more details, see 6 | # http://developer.android.com/guide/developing/tools/proguard.html 7 | 8 | # If your project uses WebView with JS, uncomment the following 9 | # and specify the fully qualified class name to the JavaScript interface 10 | # class: 11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 12 | # public *; 13 | #} 14 | 15 | # Uncomment this to preserve the line number information for 16 | # debugging stack traces. 17 | #-keepattributes SourceFile,LineNumberTable 18 | 19 | # If you keep the line number information, uncomment this to 20 | # hide the original source file name. 21 | #-renamesourcefileattribute SourceFile 22 | 23 | -keep class io.agora.**{*;} -------------------------------------------------------------------------------- /app/src/main/res/values/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "android", 9 | "request": "launch", 10 | "name": "Android launch", 11 | "appSrcRoot": "${workspaceRoot}/app/src/main", 12 | "apkFile": "${workspaceRoot}/app/build/outputs/apk/debug/app-debug.apk", 13 | "adbPort": 5037 14 | }, 15 | { 16 | "type": "android", 17 | "request": "attach", 18 | "name": "Android attach", 19 | "appSrcRoot": "${workspaceRoot}/app/src/main", 20 | "adbPort": 5037, 21 | "processId": "${command:PickAndroidProcess}" 22 | } 23 | ] 24 | } -------------------------------------------------------------------------------- /app/src/main/res/values-night/themes.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: Meherdeep 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Version [e.g. 22] 29 | 30 | **Smartphone (please complete the following information):** 31 | - Device: [e.g. Pixel 5] 32 | - OS: [e.g. Android] 33 | - Version [e.g. 11] 34 | 35 | **Additional context** 36 | Add any other context about the problem here. 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Agora.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /agorauikit_android/src/main/res/drawable/ic_video_mute.xml: -------------------------------------------------------------------------------- 1 | 6 | 9 | 12 | 13 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | # IDE (e.g. Android Studio) users: 3 | # Gradle settings configured through the IDE *will override* 4 | # any settings specified in this file. 5 | # For more details on how to configure your build environment visit 6 | # http://www.gradle.org/docs/current/userguide/build_environment.html 7 | # Specifies the JVM arguments used for the daemon process. 8 | # The setting is particularly useful for tweaking memory settings. 9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 10 | # When configured, Gradle will run in incubating parallel mode. 11 | # This option should only be used with decoupled projects. More details, visit 12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 13 | # org.gradle.parallel=true 14 | # AndroidX package structure to make it clearer which packages are bundled with the 15 | # Android operating system, and which are packaged with your app"s APK 16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn 17 | android.useAndroidX=true 18 | # Automatically convert third-party libraries to use AndroidX 19 | android.enableJetifier=true 20 | # Kotlin code style for this project: "official" or "obsolete": 21 | kotlin.code.style=official 22 | kotlin.version="1.7.21" 23 | 24 | android.disableAutomaticComponentCreation=true -------------------------------------------------------------------------------- /.idea/codestream.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /app/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.android.application") 3 | id("kotlin-android") 4 | id("org.jlleitschuh.gradle.ktlint") version "11.0.0" 5 | } 6 | 7 | android { 8 | compileSdkVersion(33) 9 | buildToolsVersion("30.0.3") 10 | 11 | defaultConfig { 12 | 13 | applicationId = "io.agora.agora_android_uikit" 14 | minSdk = 24 15 | targetSdk = 33 16 | versionCode = 1 17 | versionName = "1.0" 18 | 19 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 20 | } 21 | 22 | buildTypes { 23 | getByName("release") { 24 | isMinifyEnabled = false 25 | proguardFiles("proguard-rules.pro") 26 | } 27 | } 28 | 29 | kotlinOptions { 30 | jvmTarget = "1.8" 31 | } 32 | } 33 | 34 | dependencies { 35 | implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.10") 36 | implementation("androidx.core:core-ktx:1.9.0") 37 | implementation("androidx.appcompat:appcompat:1.5.1") 38 | implementation("com.google.android.material:material:1.7.0") 39 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 40 | 41 | implementation(project(":agorauikit_android")) 42 | testImplementation("junit:junit:4.13.2") 43 | androidTestImplementation("androidx.test.ext:junit:1.1.4") 44 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/dokka-build.yml: -------------------------------------------------------------------------------- 1 | name: Deploy docs 2 | 3 | on: 4 | push: 5 | branches: "main" 6 | paths-ignore: [ 'docs/**', 'README.md' ] 7 | 8 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 9 | permissions: 10 | contents: read 11 | pages: write 12 | id-token: write 13 | 14 | # Allow one concurrent deployment 15 | concurrency: 16 | group: "pages" 17 | cancel-in-progress: true 18 | 19 | jobs: 20 | build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - uses: actions/checkout@v3 24 | with: 25 | fetch-depth: 0 26 | - name: Set up JDK 17 27 | uses: actions/setup-java@v3 28 | with: 29 | distribution: temurin 30 | java-version: 17 31 | 32 | - name: Setup Gradle 33 | uses: gradle/gradle-build-action@v2 34 | 35 | - name: Generate API documentation 36 | run: | 37 | LIB_VERSION=$(grep 'UIKitData = UIKitData' agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraRtmController/AgoraRtmController+MuteRequest.kt | sed -e 's,.*\"\(.*\)\"),\1,') 38 | echo Generating API documentation for version $LIB_VERSION 39 | ./gradlew -Pversion=$LIB_VERSION dokkaHtml 40 | - name: Upload artifact 📜 41 | uses: actions/upload-pages-artifact@v1 42 | with: 43 | # Upload docs directory 44 | path: 'agorauikit_android/build/dokka/html' 45 | - name: Deploy to GitHub Pages 🐙 46 | id: deployment 47 | uses: actions/deploy-pages@v1 48 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 25 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 24 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/DevicePermissions.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import android.Manifest 4 | import android.app.Activity 5 | import android.content.Context 6 | import android.content.pm.PackageManager 7 | import androidx.core.app.ActivityCompat 8 | import androidx.core.content.ContextCompat 9 | 10 | private const val PERMISSION_REQ_ID = 22 11 | 12 | // Ask for Android device permissions at runtime. 13 | private val REQUESTED_PERMISSIONS = arrayOf( 14 | Manifest.permission.RECORD_AUDIO, 15 | Manifest.permission.CAMERA, 16 | Manifest.permission.WRITE_EXTERNAL_STORAGE 17 | ) 18 | 19 | /** 20 | * Request all relevant permissions 21 | * 22 | * @param context Activity Context 23 | * @return True if all the permissions were already granted 24 | */ 25 | @ExperimentalUnsignedTypes 26 | @JvmOverloads public fun AgoraVideoViewer.Companion.requestPermission(context: Context): Boolean { 27 | return checkSelfPermission(context, Manifest.permission.RECORD_AUDIO) && 28 | checkSelfPermission(context, Manifest.permission.CAMERA) 29 | } 30 | 31 | @ExperimentalUnsignedTypes 32 | /** 33 | * Requests a particular permission if not granted 34 | * 35 | * @param context Activity Context 36 | * @param permission Permission String 37 | * @return True if Permission is granted 38 | */ 39 | @JvmOverloads public fun AgoraVideoViewer.Companion.checkSelfPermission(context: Context, permission: String): Boolean { 40 | if (ContextCompat.checkSelfPermission(context, permission) != 41 | PackageManager.PERMISSION_GRANTED 42 | ) { 43 | ActivityCompat.requestPermissions( 44 | context as Activity, 45 | REQUESTED_PERMISSIONS, 46 | PERMISSION_REQ_ID 47 | ) 48 | return false 49 | } 50 | return true 51 | } 52 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_foreground.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 15 | 18 | 21 | 22 | 23 | 24 | 30 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to contribute to Agora VideoUIKit 2 | 3 | ## **Did you find a bug?** 4 | 5 | * **Ensure the bug was not already reported** by searching on GitHub under [Issues](https://github.com/AgoraIO-Community/Android-UIKit/issues). 6 | 7 | * If you're unable to find an open issue addressing the problem, [open a new one](https://github.com/AgoraIO-Community/Android-UIKit/issues/new). Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. 8 | 9 | * If possible, use the relevant bug report templates to create the issue. 10 | 11 | ## **Did you write a patch that fixes a bug?** 12 | 13 | * Open a new GitHub pull request with the patch. 14 | * Ensure the PR description clearly describes the problem and solution. Include the relevant issue number if applicable. 15 | * Ensure that the patch compiles and runs. 16 | 17 | ## **Did you fix whitespace, format code, or make a purely cosmetic patch?** 18 | 19 | Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Agora VideoUIKit will generally not be accepted. 20 | 21 | ## **Do you intend to add a new feature or change an existing one?** 22 | 23 | * Suggest your change in the form of a Feature Request Issue on this repository. 24 | 25 | * One of the team at Agora will respond to your issue, and together we can figure out whether this feature is applicable to be added to Agora VideoUIKit. 26 | 27 | ## **Do you have questions about the source code?** 28 | 29 | * Ask any question about how to use UIKit on the [RTE Dev Slack](https://www.agora.io/en/join-slack/). 30 | 31 | --- 32 | 33 | Thanks for taking the time to read through our contributor guidelines, 34 | 35 | Agora Developer Relations Team 36 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraRtmController/AgoraRtmClientHandler.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android.AgoraRtmController 2 | 3 | import io.agora.agorauikit_android.AgoraVideoViewer 4 | import io.agora.agorauikit_android.R 5 | import io.agora.rtm.RtmClientListener 6 | import io.agora.rtm.RtmMessage 7 | import java.util.logging.Level 8 | import java.util.logging.Logger 9 | /** 10 | * Class for all the Agora RTM Client event handlers 11 | * 12 | * @param hostView [AgoraVideoViewer] 13 | */ 14 | @ExperimentalUnsignedTypes 15 | class AgoraRtmClientHandler(private val hostView: AgoraVideoViewer) : RtmClientListener { 16 | 17 | val TAG = this.hostView.resources.getString(R.string.TAG) 18 | 19 | override fun onConnectionStateChanged(state: Int, reason: Int) { 20 | Logger.getLogger(TAG) 21 | .log(Level.INFO, "RTM Connection State Changed. state: $state, reason: $reason") 22 | 23 | this.hostView.rtmClientOverrideHandler?.onConnectionStateChanged(state, reason) 24 | } 25 | 26 | override fun onMessageReceived(rtmMessage: RtmMessage, peerId: String?) { 27 | AgoraRtmController.messageReceived(message = rtmMessage.text, hostView = hostView) 28 | 29 | this.hostView.rtmClientOverrideHandler?.onMessageReceived(rtmMessage, peerId) 30 | } 31 | 32 | override fun onTokenExpired() { 33 | this.hostView.rtmClientOverrideHandler?.onTokenExpired() 34 | } 35 | 36 | override fun onTokenPrivilegeWillExpire() { 37 | this.hostView.rtmClientOverrideHandler?.onTokenPrivilegeWillExpire() 38 | } 39 | 40 | override fun onPeersOnlineStatusChanged(peerStatus: MutableMap?) { 41 | Logger.getLogger(TAG).log(Level.INFO, "onPeerOnlineStatusChanged: $peerStatus") 42 | 43 | this.hostView.rtmClientOverrideHandler?.onPeersOnlineStatusChanged(peerStatus) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/caches 5 | /.idea/libraries 6 | /.idea/modules.xml 7 | /.idea/workspace.xml 8 | /.idea/navEditor.xml 9 | /.idea/assetWizardSettings.xml 10 | .DS_Store 11 | /build 12 | /captures 13 | .externalNativeBuild 14 | .cxx 15 | local.properties 16 | 17 | # Built application files 18 | *.apk 19 | *.aar 20 | *.ap_ 21 | *.aab 22 | 23 | # Files for the ART/Dalvik VM 24 | *.dex 25 | 26 | # Java class files 27 | *.class 28 | 29 | # Generated files 30 | bin/ 31 | gen/ 32 | out/ 33 | # Uncomment the following line in case you need and you don't have the release build type files in your app 34 | # release/ 35 | 36 | # Gradle files 37 | .gradle/ 38 | build/ 39 | 40 | # Local configuration file (sdk path, etc) 41 | local.properties 42 | 43 | # Proguard folder generated by Eclipse 44 | proguard/ 45 | 46 | # Log Files 47 | *.log 48 | 49 | # Android Studio Navigation editor temp files 50 | .navigation/ 51 | 52 | # Android Studio captures folder 53 | captures/ 54 | 55 | # IntelliJ 56 | *.iml 57 | .idea/workspace.xml 58 | .idea/tasks.xml 59 | .idea/gradle.xml 60 | .idea/assetWizardSettings.xml 61 | .idea/dictionaries 62 | .idea/libraries 63 | # Android Studio 3 in .gitignore file. 64 | .idea/caches 65 | .idea/modules.xml 66 | # Comment next line if keeping position of elements in Navigation Editor is relevant for you 67 | .idea/navEditor.xml 68 | 69 | # Keystore files 70 | # Uncomment the following lines if you do not want to check your keystore files in. 71 | #*.jks 72 | #*.keystore 73 | 74 | # External native build folder generated in Android Studio 2.2 and later 75 | .externalNativeBuild 76 | .cxx/ 77 | 78 | # Google Services (e.g. APIs or Firebase) 79 | # google-services.json 80 | 81 | # Freeline 82 | freeline.py 83 | freeline/ 84 | freeline_project_description.json 85 | 86 | # fastlane 87 | fastlane/report.xml 88 | fastlane/Preview.html 89 | fastlane/screenshots 90 | fastlane/test_output 91 | fastlane/readme.md 92 | 93 | # Version control 94 | vcs.xml 95 | 96 | # lint 97 | lint/intermediates/ 98 | lint/generated/ 99 | lint/outputs/ 100 | lint/tmp/ 101 | # lint/reports/ 102 | 103 | # Android Profiling 104 | *.hprof -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraRtmController/AgoraRtmChannelHandler.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android.AgoraRtmController 2 | 3 | import io.agora.agorauikit_android.AgoraVideoViewer 4 | import io.agora.agorauikit_android.R 5 | import io.agora.rtm.RtmChannelAttribute 6 | import io.agora.rtm.RtmChannelListener 7 | import io.agora.rtm.RtmChannelMember 8 | import io.agora.rtm.RtmMessage 9 | import java.util.logging.Level 10 | import java.util.logging.Logger 11 | 12 | /** 13 | * Class for all the Agora RTM Channel event handlers 14 | * 15 | * @param hostView [AgoraVideoViewer] 16 | */ 17 | @ExperimentalUnsignedTypes 18 | open class AgoraRtmChannelHandler(private val hostView: AgoraVideoViewer) : RtmChannelListener { 19 | 20 | val TAG = this.hostView.resources.getString(R.string.TAG) 21 | 22 | override fun onMemberCountUpdated(memberCount: Int) { 23 | Logger.getLogger(TAG).log(Level.INFO, "RTM member count updated : $memberCount") 24 | this.hostView.rtmChannelOverrideHandler?.onMemberCountUpdated(memberCount) 25 | } 26 | override fun onAttributesUpdated(attributeList: MutableList?) { 27 | Logger.getLogger(TAG).log(Level.INFO, "RTM Channel attributes updated") 28 | this.hostView.rtmChannelOverrideHandler?.onAttributesUpdated(attributeList) 29 | } 30 | override fun onMessageReceived( 31 | rtmMessage: RtmMessage, 32 | rtmChannelMember: RtmChannelMember 33 | ) { 34 | Logger.getLogger(TAG).log(Level.INFO, "RTM Channel Message Received") 35 | AgoraRtmController.messageReceived(rtmMessage.text, hostView) 36 | this.hostView.rtmChannelOverrideHandler?.onMessageReceived(rtmMessage, rtmChannelMember) 37 | } 38 | 39 | override fun onMemberJoined(rtmChannelMember: RtmChannelMember) { 40 | Logger.getLogger(TAG).log(Level.SEVERE, "RTM member : ${rtmChannelMember.userId} joined channel : ${rtmChannelMember.channelId}") 41 | AgoraRtmController.sendUserData(toChannel = false, peerRtmId = rtmChannelMember.userId, hostView = this.hostView) 42 | 43 | this.hostView.rtmChannelOverrideHandler?.onMemberJoined(rtmChannelMember) 44 | } 45 | override fun onMemberLeft(rtmChannelMember: RtmChannelMember) { 46 | Logger.getLogger(TAG).log(Level.SEVERE, "RTM member left ${rtmChannelMember.userId} from channel ${rtmChannelMember.channelId}") 47 | 48 | this.hostView.rtmChannelOverrideHandler?.onMemberLeft(rtmChannelMember) 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraRtmController/AgoraRtmController+Tokens.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android.AgoraRtmController 2 | 3 | import okhttp3.Call 4 | import okhttp3.Callback 5 | import okhttp3.OkHttpClient 6 | import okhttp3.Request 7 | import okhttp3.Response 8 | import org.json.JSONException 9 | import org.json.JSONObject 10 | import java.io.IOException 11 | import java.util.logging.Level 12 | import java.util.logging.Logger 13 | 14 | public interface RtmTokenCallback { 15 | fun onSuccess(token: String) 16 | fun onError(error: RtmTokenError) 17 | } 18 | 19 | /** 20 | * Error types to expect from fetchToken on failing ot retrieve valid token. 21 | */ 22 | enum class RtmTokenError { 23 | NO_DATA, INVALID_DATA, INVALID_URL, UNKNOWN 24 | } 25 | 26 | @ExperimentalUnsignedTypes 27 | fun AgoraRtmController.Companion.fetchToken(urlBase: String, rtmId: String, completion: RtmTokenCallback) { 28 | val log: Logger = Logger.getLogger("AgoraVideoUIKit") 29 | val client = OkHttpClient() 30 | val url = "$urlBase/rtm/$rtmId" 31 | val request: okhttp3.Request = Request.Builder() 32 | .url(url) 33 | .method("GET", null) 34 | .build() 35 | 36 | try { 37 | client.newCall(request).enqueue(object : Callback { 38 | override fun onFailure(call: Call, e: IOException) { 39 | log.log(Level.WARNING, "Unexpected code ${e.localizedMessage}") 40 | completion.onError(RtmTokenError.INVALID_DATA) 41 | } 42 | 43 | override fun onResponse(call: Call, response: Response) { 44 | response.body?.string()?.let { 45 | val jObject = JSONObject(it) 46 | val token = jObject.getString("rtmToken") 47 | if (!token.isEmpty()) { 48 | completion.onSuccess(token) 49 | return 50 | } 51 | } 52 | completion.onError(RtmTokenError.NO_DATA) 53 | } 54 | }) 55 | } catch (e: IOException) { 56 | log.log(Level.WARNING, e.localizedMessage) 57 | completion.onError(RtmTokenError.INVALID_URL) 58 | } catch (e: JSONException) { 59 | log.log(Level.WARNING, e.localizedMessage) 60 | completion.onError(RtmTokenError.NO_DATA) 61 | } catch (e: Exception) { 62 | log.log(Level.WARNING, e.message) 63 | completion.onError(RtmTokenError.UNKNOWN) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraRtmController/AgoraRtmController+Helpers.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android.AgoraRtmController 2 | 3 | import android.graphics.Color 4 | import com.google.android.material.snackbar.Snackbar 5 | import io.agora.agorauikit_android.AgoraButton 6 | import io.agora.agorauikit_android.AgoraVideoViewer 7 | import kotlinx.serialization.decodeFromString 8 | import kotlinx.serialization.json.Json 9 | import org.json.JSONObject 10 | 11 | @ExperimentalUnsignedTypes 12 | fun AgoraRtmController.Companion.messageReceived(message: String, hostView: AgoraVideoViewer) { 13 | val messageMap = JSONObject(message) 14 | when (messageMap.getString("messageType")) { 15 | "UserData" -> { 16 | val userData = Json.decodeFromString(message) 17 | val rtmId = userData.rtmId 18 | userData.rtcId?.let { rtcId -> 19 | hostView.agoraSettings.uidToUserIdMap.putIfAbsent(rtcId, rtmId) 20 | } 21 | hostView.agoraSettings.userRtmMap.putIfAbsent(rtmId, userData) 22 | } 23 | "MuteRequest" -> { 24 | val muteRequest = Json.decodeFromString(message) 25 | val deviceType = DeviceType.fromInt(muteRequest.device) 26 | val snackbar = Snackbar.make( 27 | hostView, 28 | if (deviceType == DeviceType.MIC) 29 | "Please " + (if (muteRequest.mute) "" else "un") + "mute your mic" 30 | else 31 | "Please " + (if (muteRequest.mute) "dis" else "en") + "able your camera", 32 | Snackbar.LENGTH_LONG 33 | ) 34 | snackbar.setAction( 35 | if (deviceType == DeviceType.MIC) 36 | if (muteRequest.mute) "mute" else "unmute" 37 | else if (muteRequest.mute) "disable" else "enable" 38 | ) { 39 | var changingButton: AgoraButton? 40 | var isMuted = muteRequest.mute 41 | if (deviceType == DeviceType.MIC) { 42 | hostView.agkit.muteLocalAudioStream(isMuted) 43 | changingButton = hostView.micButton 44 | hostView.userVideoLookup[hostView.userID]?.audioMuted = isMuted 45 | } else { 46 | hostView.agkit.enableLocalVideo(!isMuted) 47 | changingButton = hostView.camButton 48 | hostView.userVideoLookup[hostView.userID]?.videoMuted = isMuted 49 | } 50 | changingButton?.isSelected = isMuted 51 | changingButton?.background?.setTint(if (isMuted) Color.RED else Color.GRAY) 52 | } 53 | snackbar.show() 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /agorauikit_android/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("maven-publish") 3 | // id("maven") 4 | id("com.android.library") 5 | id("kotlin-android") 6 | id("org.jetbrains.dokka") 7 | id("org.jlleitschuh.gradle.ktlint") 8 | kotlin("plugin.serialization") version "1.4.10" 9 | } 10 | // group = "com.github.agoraio-community" 11 | 12 | android { 13 | compileSdkVersion(33) 14 | buildToolsVersion("30.0.3") 15 | 16 | defaultConfig { 17 | minSdk = 24 18 | targetSdk = 32 19 | 20 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 21 | consumerProguardFiles("consumer-rules.pro") 22 | } 23 | 24 | buildTypes { 25 | getByName("release") { 26 | isMinifyEnabled = false 27 | proguardFiles("proguard-rules.pro") 28 | } 29 | } 30 | kotlinOptions { 31 | jvmTarget = "1.8" 32 | } 33 | } 34 | 35 | // Because the components are created only during the afterEvaluate phase, you must 36 | // configure your publications using the afterEvaluate() lifecycle method. 37 | afterEvaluate { 38 | publishing { 39 | publications { 40 | create("release") { 41 | // Applies the component for the release build variant. 42 | from(components["release"]) 43 | groupId = "com.github.agoraio-community" 44 | artifactId = "final" 45 | version = "2.0.6" 46 | } 47 | // Creates a Maven publication called “debug”. 48 | create("debug") { 49 | // Applies the component for the debug build variant. 50 | from(components["debug"]) 51 | groupId = "com.github.agoraio-community" 52 | artifactId = "final-debug" 53 | version = "2.0.6" 54 | } 55 | } 56 | } 57 | } 58 | 59 | dependencies { 60 | implementation("androidx.constraintlayout:constraintlayout:2.1.4") 61 | implementation("androidx.recyclerview:recyclerview:1.2.1") 62 | implementation("com.google.android.material:material:1.7.0") 63 | implementation("org.jetbrains.kotlin:kotlin-stdlib:1.7.10") 64 | implementation("androidx.core:core-ktx:1.9.0") 65 | implementation("androidx.appcompat:appcompat:1.5.1") 66 | api("io.agora.rtc:full-sdk:4.1.1") 67 | api("io.agora.rtm:rtm-sdk:1.5.3") 68 | implementation("com.squareup.okhttp3:okhttp:4.10.0") 69 | implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.2") 70 | testImplementation("junit:junit:4.13.2") 71 | androidTestImplementation("androidx.test.ext:junit:1.1.4") 72 | androidTestImplementation("androidx.test.espresso:espresso-core:3.5.0") 73 | } 74 | tasks.dokkaHtml.configure { 75 | suppressInheritedMembers.set(true) 76 | } 77 | -------------------------------------------------------------------------------- /.idea/jarRepositories.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 14 | 15 | 19 | 20 | 24 | 25 | 29 | 30 | 34 | 35 | 39 | 40 | 44 | 45 | 49 | 50 | 54 | 55 | 59 | 60 | 64 | 65 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | > Release Version: 4 | 5 | ## Release Notes 6 | 7 | - 8 | - 9 | 10 | ## Pull request checklist 11 | 12 | Please check if your PR fulfills the following requirements: 13 | - [ ] Tests for the changes have been added (for bug fixes / features) 14 | - [ ] Docs have been reviewed and added / updated if needed (for bug fixes / features) 15 | - [ ] The GitHub Actions pass building and linting. Linter returns no warnings or errors. 16 | - [ ] The QA checklist below has been completed 17 | 18 | ## Pull request type 19 | 20 | 21 | 22 | 23 | 24 | Please check the type of change your PR introduces: 25 | - [ ] Bugfix 26 | - [ ] Feature 27 | - [ ] Code style update (formatting, renaming) 28 | - [ ] Refactoring (no functional changes, no api changes) 29 | - [ ] Build related changes 30 | - [ ] Documentation content changes 31 | - [ ] Other (please describe): 32 | 33 | 34 | ## What is the current behavior? 35 | 36 | 37 | Issue Number: N/A 38 | 39 | 40 | ## What is the new behavior? 41 | 42 | 43 | - 44 | - 45 | - 46 | 47 | ## Does this introduce a breaking change? 48 | 49 | - [ ] Yes 50 | - [ ] No 51 | 52 | 53 | 54 | 55 | ## QA Checklist 56 | 57 | ### UIKit Update Checklist (Minor or Patch Release) 58 | 59 | - [ ] Updated version number in `agorauikit_android/build.gradle.kts` 60 | - [ ] Using the latest version of Agora's Video SDK 61 | - [ ] Example apps are all functional 62 | - [ ] Core features are still working (both ways across platforms) 63 | - [ ] Camera + Mic muting works for local and remote users 64 | - [ ] Users are added and removed correctly when they join and leave the channel 65 | - [ ] Older versions of the library gracefully handle changes (Create issue if not) 66 | - [ ] Builtin buttons all work as expected 67 | - [ ] Any newly deprecated methods are flagged as such inline and in documentation 68 | 69 | 70 | 71 | ### UIKit Update Checklist (Major Release) 72 | 73 | - [ ] The above checklist is completed (except backwards compatibility) 74 | - [ ] Thoroughly tested for crashes, across multiple platforms at the same time 75 | 76 | #### QA Notes 77 | 78 | ## Other information 79 | 80 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraButton.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import android.content.Context 4 | import android.graphics.Color 5 | import android.util.AttributeSet 6 | import android.view.View 7 | import android.view.ViewGroup 8 | import androidx.annotation.DimenRes 9 | import androidx.core.view.setPadding 10 | 11 | /** 12 | * A button to fit the style of builtin Agora VideoUIKit Buttons 13 | * 14 | * @param context the context for the application. 15 | * @param attrs the attribute set for the button. 16 | * @param defStyleAttr An attribute in the current theme that contains a reference to a style resource that supplies defaults values for the StyledAttributes. Can be 0 to not look for defaults. 17 | * @property clickAction The action to be conducted when the button is tapped. 18 | * @constructor Creates a new button. 19 | */ 20 | public class AgoraButton @JvmOverloads constructor( 21 | context: Context, 22 | attrs: AttributeSet? = null, 23 | defStyleAttr: Int = 0 24 | ) : androidx.appcompat.widget.AppCompatImageView(context, attrs, defStyleAttr) { 25 | 26 | public var clickAction: ((button: AgoraButton) -> Any?)? = null 27 | 28 | init { 29 | // setBackgroundColor(Color.BLUE) 30 | background = context.getDrawable(R.drawable.button_background) 31 | scaleType = ScaleType.FIT_XY 32 | this.background.setTint(Color.GRAY) 33 | setPadding(DPToPx(context, 5)) 34 | setOnClickListener { 35 | this.clickAction?.let { it(this) } 36 | } 37 | } 38 | 39 | override fun onAttachedToWindow() { 40 | super.onAttachedToWindow() 41 | setMargin(R.dimen.button_margin, R.dimen.button_margin, R.dimen.button_margin, R.dimen.button_margin) 42 | } 43 | 44 | private fun View?.setMargin( 45 | @DimenRes marginStart: Int? = null, 46 | @DimenRes marginTop: Int? = null, 47 | @DimenRes marginEnd: Int? = null, 48 | @DimenRes marginBottom: Int? = null 49 | ) { 50 | setMarginPixelOffset( 51 | marginStart?.let { 52 | this?.resources?.getDimensionPixelOffset(it) 53 | }, 54 | marginTop?.let { 55 | this?.resources?.getDimensionPixelOffset(it) 56 | }, 57 | marginEnd?.let { 58 | this?.resources?.getDimensionPixelOffset(it) 59 | }, 60 | marginBottom?.let { 61 | this?.resources?.getDimensionPixelOffset(it) 62 | } 63 | ) 64 | } 65 | 66 | private fun View?.setMarginPixelOffset( 67 | marginStart: Int? = null, 68 | marginTop: Int? = null, 69 | marginEnd: Int? = null, 70 | marginBottom: Int? = null 71 | ) { 72 | 73 | (this?.layoutParams as? ViewGroup.MarginLayoutParams)?.let { mlp -> 74 | mlp.setMargins( 75 | marginStart ?: mlp.marginStart, 76 | marginTop ?: mlp.topMargin, 77 | marginEnd ?: mlp.marginEnd, 78 | marginBottom ?: mlp.bottomMargin 79 | ) 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraVideoViewer+Token.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import okhttp3.Call 4 | import okhttp3.Callback 5 | import okhttp3.OkHttpClient 6 | import okhttp3.Request 7 | import okhttp3.Response 8 | import org.json.JSONException 9 | import org.json.JSONObject 10 | import java.io.IOException 11 | import java.util.logging.Level 12 | import java.util.logging.Logger 13 | 14 | public interface TokenCallback { 15 | fun onSuccess(token: String) 16 | fun onError(error: TokenError) 17 | } 18 | 19 | /** 20 | * Error types to expect from fetchToken on failing ot retrieve valid token. 21 | */ 22 | enum class TokenError { 23 | NODATA, INVALIDDATA, INVALIDURL 24 | } 25 | 26 | /** 27 | * Requests the token from our backend token service 28 | * @param urlBase: base URL specifying where the token server is located 29 | * @param channelName: Name of the channel we're requesting for 30 | * @param userId: User ID of the user trying to join (0 for any user) 31 | * @param callback: Callback method for returning either the string token or error 32 | */ 33 | @ExperimentalUnsignedTypes 34 | fun AgoraVideoViewer.Companion.fetchToken(urlBase: String, channelName: String, userId: Int, completion: TokenCallback) { 35 | val log: Logger = Logger.getLogger("AgoraVideoUIKit") 36 | val client = OkHttpClient() 37 | val url = "$urlBase/rtc/$channelName/publisher/uid/$userId/" 38 | val request: okhttp3.Request = Request.Builder() 39 | .url(url) 40 | .method("GET", null) 41 | .build() 42 | try { 43 | client.newCall(request).enqueue(object : Callback { 44 | override fun onFailure(call: Call, e: IOException) { 45 | log.log(Level.WARNING, "Unexpected code ${e.localizedMessage}") 46 | completion.onError(TokenError.INVALIDDATA) 47 | } 48 | 49 | override fun onResponse(call: Call, response: Response) { 50 | response.body?.string()?.let { 51 | val jObject = JSONObject(it) 52 | val token = jObject.getString("rtcToken") 53 | if (token.isNotEmpty()) { 54 | completion.onSuccess(token) 55 | return 56 | } 57 | } 58 | completion.onError(TokenError.NODATA) 59 | } 60 | } 61 | ) 62 | } catch (e: IOException) { 63 | log.log(Level.WARNING, e.localizedMessage) 64 | completion.onError(TokenError.INVALIDURL) 65 | } catch (e: JSONException) { 66 | log.log(Level.WARNING, e.localizedMessage) 67 | completion.onError(TokenError.INVALIDDATA) 68 | } 69 | } 70 | 71 | /** 72 | * Renews the token before the default expiry time or the specified time 73 | */ 74 | @ExperimentalUnsignedTypes 75 | internal fun AgoraVideoViewer.fetchRenewToken() { 76 | (this.agoraSettings.tokenURL)?.let { tokenURL -> 77 | this.connectionData.channel?.let { channelName -> 78 | val callback: TokenCallback = object : TokenCallback { 79 | override fun onSuccess(token: String) { 80 | this@fetchRenewToken.agkit.renewToken(token) 81 | } 82 | 83 | override fun onError(error: TokenError) { 84 | Logger.getLogger("AgoraVideoUIKit", error.name) 85 | } 86 | } 87 | 88 | AgoraVideoViewer.fetchToken( 89 | tokenURL, 90 | channelName, 91 | this.userID, 92 | callback 93 | ) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [ARCHIVED] Agora VideoUIKit for Android 2 | 3 | **⚠️ This project is no longer maintained and has been archived.** 4 | Please note that this repository is now in a read-only state and will not receive any further updates or support. 5 | We recommend migrating to the following alternatives: 6 | 7 | - **Agora SDK**: For developers seeking a customizable solution with full control over the user experience. [Learn more](https://www.agora.io/en/products/video-call/) 8 | - **Agora App Builder**: For those preferring a no-code approach to integrate real-time engagement features. [Get started](https://appbuilder.agora.io/) 9 | 10 | For documentation and support, please visit the [Agora Documentation](https://docs.agora.io/en/). 11 | 12 |

13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

21 | 22 | 23 | Instantly integrate Agora in your own Android application or prototype. 24 | 25 |

26 |

27 | 28 | [See documentation here](https://agoraio-community.github.io/VideoUIKit-Android/agorauikit_android/io.agora.agorauikit_android/index.html). 29 | 30 | ## Requirements 31 | 32 | - Android 24+ 33 | - Android Studio 34 | - [An Agora developer account](https://www.agora.io/en/blog/how-to-get-started-with-agora?utm_source=github&utm_repo=agora-android-uikit) 35 | 36 | ## Installation 37 | 38 | **Step 1:** Add it in your root build.gradle at the end of repositories: 39 | 40 | ```css 41 | allprojects { 42 | repositories { 43 | ... 44 | maven { url 'https://jitpack.io' } 45 | } 46 | } 47 | ``` 48 | 49 | **Step 2:** Add the dependency 50 | 51 | ```css 52 | dependencies { 53 | implementation 'com.github.AgoraIO-Community:VideoUIKit-Android:version' 54 | } 55 | ``` 56 | 57 | Then sync gradle build files. More information on [JitPack](https://jitpack.io/#AgoraIO-Community/VideoUIKit-Android). 58 | 59 | ## Usage 60 | 61 | Once installed, you can add the AgoraVideoViewer from within the context of your MainActivity like so: 62 | 63 | ```kotlin 64 | // Create AgoraVideoViewer instance 65 | val agView = AgoraVideoViewer(this, AgoraConnectionData("my-app-id")) 66 | // Fill the parent ViewGroup (MainActivity) 67 | this.addContentView( 68 | agView, 69 | FrameLayout.LayoutParams( 70 | FrameLayout.LayoutParams.MATCH_PARENT, 71 | FrameLayout.LayoutParams.MATCH_PARENT 72 | ) 73 | ) 74 | ``` 75 | 76 | To join a channel, simply call: 77 | 78 | ```kotlin 79 | agView.join("test", role=Constants.CLIENT_ROLE_BROADCASTER) 80 | ``` 81 | 82 | ### Roadmap 83 | 84 | - [x] Muting/Unmuting a remote member 85 | - [ ] Usernames 86 | - [ ] Promoting an audience member to a broadcaster role. 87 | - [ ] Layout for Voice Calls 88 | - [ ] Cloud recording 89 | 90 | ## VideoUIKits 91 | 92 | The plan is to grow this library and have similar offerings across all supported platforms. There are already similar libraries for [Flutter](https://github.com/AgoraIO-Community/VideoUIKit-Flutter/), [React Native](https://github.com/AgoraIO-Community/VideoUIKit-ReactNative), and [iOS](https://github.com/AgoraIO-Community/VideoUIKit-iOS/), so be sure to check them out. 93 | -------------------------------------------------------------------------------- /app/src/main/java/io/agora/agora_android_uikit/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agora_android_uikit 2 | 3 | import android.Manifest 4 | import android.graphics.Color 5 | import android.os.Bundle 6 | import android.util.Log 7 | import android.util.Log.println 8 | import android.view.ViewGroup 9 | import android.widget.Button 10 | import android.widget.FrameLayout 11 | import androidx.appcompat.app.AppCompatActivity 12 | import io.agora.agorauikit_android.AgoraButton 13 | import io.agora.agorauikit_android.AgoraConnectionData 14 | import io.agora.agorauikit_android.AgoraSettings 15 | import io.agora.agorauikit_android.AgoraVideoViewer 16 | import io.agora.agorauikit_android.requestPermission 17 | import io.agora.rtc2.Constants 18 | 19 | // Ask for Android device permissions at runtime. 20 | private const val PERMISSION_REQ_ID = 22 21 | private val REQUESTED_PERMISSIONS = arrayOf( 22 | Manifest.permission.RECORD_AUDIO, 23 | Manifest.permission.CAMERA, 24 | Manifest.permission.WRITE_EXTERNAL_STORAGE 25 | ) 26 | 27 | @ExperimentalUnsignedTypes 28 | class MainActivity : AppCompatActivity() { 29 | var agView: AgoraVideoViewer? = null 30 | override fun onCreate(savedInstanceState: Bundle?) { 31 | super.onCreate(savedInstanceState) 32 | setContentView(R.layout.activity_main) 33 | try { 34 | agView = AgoraVideoViewer( 35 | this, AgoraConnectionData("my-app-id"), 36 | agoraSettings = this.settingsWithExtraButtons() 37 | ) 38 | } catch (e: Exception) { 39 | println(Log.ERROR, "VideoUIKit App", "Could not initialise AgoraVideoViewer. Check your App ID is valid. ${e.message}") 40 | return 41 | } 42 | val set = FrameLayout.LayoutParams( 43 | FrameLayout.LayoutParams.MATCH_PARENT, 44 | FrameLayout.LayoutParams.MATCH_PARENT 45 | ) 46 | 47 | this.addContentView(agView, set) 48 | 49 | // Check that the camera and mic permissions are accepted before attempting to join 50 | if (AgoraVideoViewer.requestPermission(this)) { 51 | agView!!.join("test", role = Constants.CLIENT_ROLE_BROADCASTER) 52 | } else { 53 | val joinButton = Button(this) 54 | joinButton.text = "Allow Camera and Microphone, then click here" 55 | joinButton.setOnClickListener { 56 | // When the button is clicked, check permissions again and join channel 57 | // if permissions are granted. 58 | if (AgoraVideoViewer.requestPermission(this)) { 59 | (joinButton.parent as ViewGroup).removeView(joinButton) 60 | agView!!.join("test", role = Constants.CLIENT_ROLE_BROADCASTER) 61 | } 62 | } 63 | joinButton.setBackgroundColor(Color.GREEN) 64 | joinButton.setTextColor(Color.RED) 65 | this.addContentView( 66 | joinButton, 67 | FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 300) 68 | ) 69 | } 70 | } 71 | 72 | fun settingsWithExtraButtons(): AgoraSettings { 73 | val agoraSettings = AgoraSettings() 74 | 75 | val agBeautyButton = AgoraButton(this) 76 | agBeautyButton.clickAction = { 77 | it.isSelected = !it.isSelected 78 | agBeautyButton.setImageResource( 79 | if (it.isSelected) android.R.drawable.star_on else android.R.drawable.star_off 80 | ) 81 | it.background.setTint(if (it.isSelected) Color.GREEN else Color.GRAY) 82 | this.agView?.agkit?.setBeautyEffectOptions(it.isSelected, this.agView?.beautyOptions) 83 | } 84 | agBeautyButton.setImageResource(android.R.drawable.star_off) 85 | 86 | agoraSettings.extraButtons = mutableListOf(agBeautyButton) 87 | 88 | return agoraSettings 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 119 | 120 | 122 | 123 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraSingleVideoView.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.view.Gravity 7 | import android.view.SurfaceView 8 | import android.view.View 9 | import android.widget.FrameLayout 10 | import android.widget.ImageView 11 | import androidx.constraintlayout.widget.ConstraintLayout 12 | import io.agora.rtc2.video.VideoCanvas 13 | 14 | /** 15 | * View for the individual Agora Camera Feed. 16 | */ 17 | @ExperimentalUnsignedTypes 18 | class AgoraSingleVideoView(context: Context, uid: Int, micColor: Int) : FrameLayout(context) { 19 | 20 | /** 21 | * Canvas used to render the Agora RTC Video. 22 | */ 23 | var canvas: VideoCanvas 24 | internal set 25 | internal var uid: Int = uid 26 | // internal var textureView: AgoraTextureView = AgoraTextureView(context) 27 | 28 | /** 29 | * Is the microphone muted for this user. 30 | */ 31 | var audioMuted: Boolean = true 32 | set(value: Boolean) { 33 | field = value 34 | (context as Activity).runOnUiThread { 35 | this.mutedFlag.visibility = if (value) VISIBLE else INVISIBLE 36 | } 37 | } 38 | 39 | /** 40 | * Is the video turned off for this user. 41 | */ 42 | var videoMuted: Boolean = true 43 | set(value: Boolean) { 44 | if (this.videoMuted != value) { 45 | this.backgroundView.visibility = if (!value) INVISIBLE else VISIBLE 46 | // this.textureView.visibility = if (value) INVISIBLE else VISIBLE 47 | } 48 | field = value 49 | } 50 | 51 | internal val hostingView: View 52 | get() { 53 | return this.canvas.view 54 | } 55 | 56 | /** 57 | * Icon to show if this user is muting their microphone 58 | */ 59 | var mutedFlag: ImageView 60 | var backgroundView: FrameLayout 61 | var micFlagColor: Int = micColor 62 | 63 | /** 64 | * Create a new AgoraSingleVideoView to be displayed in your app 65 | * @param uid: User ID of the `AgoraRtcVideoCanvas` inside this view 66 | * @param micColor: Color to be applied when the local or remote user mutes their microphone 67 | */ 68 | init { 69 | 70 | val surfaceView = SurfaceView(getContext()) 71 | this.canvas = VideoCanvas(surfaceView) 72 | this.canvas.uid = uid 73 | addView(surfaceView, ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_PARENT, ConstraintLayout.LayoutParams.MATCH_PARENT)) 74 | this.backgroundView = FrameLayout(context) 75 | this.setBackground() 76 | this.mutedFlag = ImageView(context) 77 | this.setupMutedFlag() 78 | 79 | this.layoutParams = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT) 80 | } 81 | 82 | private fun setupMutedFlag() { 83 | 84 | val mutedLayout = FrameLayout.LayoutParams(DPToPx(context, 40), DPToPx(context, 40)) 85 | // mutedLayout.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM) 86 | // mutedLayout.gravity = Gravity.RIGHT 87 | mutedLayout.gravity = Gravity.BOTTOM 88 | // mutedLayout.addRule(RelativeLayout.ALIGN_PARENT_RIGHT) 89 | mutedLayout.bottomMargin = DPToPx(context, 5) 90 | mutedLayout.leftMargin = DPToPx(context, 5) 91 | 92 | mutedFlag.setImageResource(android.R.drawable.stat_notify_call_mute) 93 | 94 | mutedFlag.setColorFilter(this.micFlagColor) 95 | addView(mutedFlag, mutedLayout) 96 | this.audioMuted = true 97 | } 98 | 99 | fun setBackground() { 100 | backgroundView.layoutParams = FrameLayout.LayoutParams( 101 | FrameLayout.LayoutParams.MATCH_PARENT, 102 | FrameLayout.LayoutParams.MATCH_PARENT 103 | ) 104 | backgroundView.setBackgroundColor(Color.LTGRAY) 105 | addView(backgroundView) 106 | val personIcon = ImageView(context) 107 | personIcon.setImageResource(R.drawable.ic_person) 108 | val buttonLayout = FrameLayout.LayoutParams(100, 100) 109 | buttonLayout.gravity = Gravity.CENTER 110 | backgroundView.addView(personIcon, buttonLayout) 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-v24/ic_launcher_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 10 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 26 | 28 | 30 | 32 | 34 | 36 | 38 | 40 | 42 | 44 | 46 | 48 | 50 | 52 | 54 | 56 | 58 | 60 | 62 | 64 | 66 | 68 | 70 | 72 | 74 | 75 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraSettings.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import android.graphics.Color 4 | import io.agora.agorauikit_android.AgoraRtmController.UserData 5 | import io.agora.rtc2.Constants 6 | import io.agora.rtc2.video.VideoEncoderConfiguration 7 | 8 | /** 9 | * Settings used for the display and behaviour of AgoraVideoViewer 10 | */ 11 | class AgoraSettings { 12 | 13 | /** 14 | * Maps user RTM ID to the user data 15 | */ 16 | internal var userRtmMap = mutableMapOf() 17 | 18 | /** 19 | * Maps RTC ID to RTM ID 20 | */ 21 | internal var uidToUserIdMap = mutableMapOf() 22 | 23 | /** 24 | * Whether RTM should be initialised and used 25 | */ 26 | public var rtmEnabled: Boolean = true 27 | /** URL to fetch tokens from. If supplied, this package will automatically fetch tokens 28 | * when the Agora Engine indicates it will be needed. 29 | * It will follow the URL pattern found in 30 | * [AgoraIO-Community/agora-token-service](https://github.com/AgoraIO-Community/agora-token-service) 31 | */ 32 | public var tokenURL: String? = null 33 | 34 | /** 35 | * Position, top, left, bottom or right. 36 | */ 37 | public enum class Position { 38 | /** 39 | * At the top of the view 40 | */ 41 | TOP, 42 | 43 | /** 44 | * At the right of the view 45 | */ 46 | RIGHT, 47 | 48 | /** 49 | * At the bottom of the view 50 | */ 51 | BOTTOM, 52 | 53 | /** 54 | * At the left of the view 55 | */ 56 | LEFT 57 | } 58 | 59 | /** 60 | * Enum value for all the default buttons offered by the VideoUIKit 61 | */ 62 | public enum class BuiltinButton { 63 | CAMERA, 64 | MIC, 65 | FLIP, 66 | END 67 | } 68 | /** 69 | * The rendering mode of the video view for all videos within the view. 70 | */ 71 | public var videoRenderMode = Constants.RENDER_MODE_FIT 72 | /** 73 | * Where the buttons such as camera enable/disable should be positioned within the view. 74 | * TODO: This is not yet implemented 75 | */ 76 | public var buttonPosition = Position.BOTTOM 77 | /** 78 | * Where the floating collection view of video members be positioned within the view. 79 | * TODO: This is not yet implemented 80 | */ 81 | public var floatPosition = Position.TOP 82 | /** 83 | * Agora's video encoder configuration. 84 | */ 85 | public var videoConfiguration: VideoEncoderConfiguration = VideoEncoderConfiguration() 86 | /** 87 | * Which buttons should be enabled in this AgoraVideoView. 88 | */ 89 | public var enabledButtons: MutableSet = mutableSetOf( 90 | BuiltinButton.CAMERA, BuiltinButton.MIC, BuiltinButton.FLIP, BuiltinButton.END 91 | ) 92 | 93 | /** 94 | * Colors for views inside AgoraVideoViewer 95 | */ 96 | public var colors = AgoraViewerColors() 97 | /** 98 | * Full string for low bitrate stream parameter, including key of `che.video.lowBitRateStreamParameter`. 99 | */ 100 | public var lowBitRateStream: String? = null 101 | /** 102 | * Maximum number of videos in the grid view before the low bitrate is adopted. 103 | */ 104 | public var gridThresholdHighBitrate = 5 105 | 106 | /** 107 | * Whether we are using dual stream mode, which helps to reduce Agora costs. 108 | */ 109 | public var usingDualStream: Boolean 110 | get() = this.lowBitRateStream != null 111 | set(newValue) { 112 | if (newValue && this.lowBitRateStream != null) { 113 | return 114 | } 115 | if (newValue) { 116 | this.lowBitRateStream = defaultLowBitrateParam 117 | } else { 118 | this.lowBitRateStream = null 119 | } 120 | } 121 | 122 | /** 123 | * A mutable list to add buttons to the default list of [BuiltinButton] 124 | */ 125 | public var extraButtons: MutableList = mutableListOf() 126 | companion object { 127 | private const val defaultLowBitrateParam = "{\"che.video.lowBitRateStreamParameter\":{\"width\":320,\"height\":180,\"frameRate\":5,\"bitRate\":140}}" 128 | } 129 | } 130 | 131 | /** 132 | * Colors for various views inside AgoraVideoViewer 133 | */ 134 | class AgoraViewerColors { 135 | /** 136 | * Color of the view that signals a user has their mic muted. Default `Color.BLUE` 137 | */ 138 | var micFlag: Int = Color.BLUE 139 | /** 140 | * Background colour of the scrollable floating viewer 141 | */ 142 | var floatingBackgroundColor: Int = Color.LTGRAY 143 | /** 144 | * Opacity of the floating viewer background (0-255) 145 | */ 146 | var floatingBackgroundAlpha: Int = 100 147 | /** 148 | * Background colour of the button holder 149 | */ 150 | var buttonBackgroundColor: Int = Color.LTGRAY 151 | /** 152 | * Opacity of the button holder background (0-255) 153 | */ 154 | var buttonBackgroundAlpha: Int = 255 / 5 155 | } 156 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraRtmController/AgoraRtmController.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android.AgoraRtmController 2 | 3 | import android.content.Context 4 | import io.agora.agorauikit_android.AgoraVideoViewer 5 | import io.agora.agorauikit_android.R 6 | import io.agora.rtm.ErrorInfo 7 | import io.agora.rtm.ResultCallback 8 | import io.agora.rtm.RtmClient 9 | import java.util.logging.Level 10 | import java.util.logging.Logger 11 | 12 | @ExperimentalUnsignedTypes 13 | class AgoraRtmController( 14 | private val hostView: AgoraVideoViewer 15 | ) { 16 | private var generatedRtmId: String? = null 17 | private var isInRtmChannel: Boolean = false 18 | 19 | /** 20 | * Enum for the Login Status of a user to Agora RTM 21 | */ 22 | public enum class LoginStatus { 23 | OFFLINE, LOGGING_IN, LOGGED_IN, LOGIN_FAILED 24 | } 25 | 26 | public var loginStatus: LoginStatus = LoginStatus.OFFLINE 27 | 28 | companion object {} 29 | 30 | val TAG = this.hostView.resources.getString(R.string.TAG) 31 | 32 | /** 33 | * Initializes the Agora RTM SDK 34 | */ 35 | fun initAgoraRtm(context: Context) { 36 | try { 37 | this.hostView.agRtmClient = 38 | RtmClient.createInstance( 39 | context, 40 | hostView.connectionData.appId, 41 | this.hostView.agoraRtmClientHandler 42 | ) 43 | } catch (e: Exception) { 44 | Logger.getLogger(TAG) 45 | .log(Level.SEVERE, "Failed to initialize Agora RTM SDK. Error: $e") 46 | } 47 | } 48 | 49 | /** 50 | * Function to login to Agora RTM 51 | */ 52 | fun loginToRtm() { 53 | if (this.hostView.connectionData.rtmId.isNullOrEmpty()) { 54 | generateRtmId() 55 | } 56 | if (loginStatus != LoginStatus.LOGGED_IN && hostView.isAgRtmClientInitialized()) { 57 | loginStatus = LoginStatus.LOGGING_IN 58 | Logger.getLogger(TAG) 59 | .log(Level.INFO, "Trying to do RTM login") 60 | this.hostView.agRtmClient.login( 61 | this.hostView.connectionData.rtmToken, 62 | this.hostView.connectionData.rtmId, 63 | object : ResultCallback { 64 | override fun onSuccess(responseInfo: Void?) { 65 | loginStatus = LoginStatus.LOGGED_IN 66 | Logger.getLogger(TAG) 67 | .log(Level.INFO, "RTM user logged in successfully") 68 | if (!isInRtmChannel) { 69 | createRtmChannel() 70 | } 71 | } 72 | 73 | override fun onFailure(errorInfo: ErrorInfo) { 74 | loginStatus = LoginStatus.LOGIN_FAILED 75 | Logger.getLogger(TAG) 76 | .log(Level.SEVERE, "RTM user login failed. Error: $errorInfo") 77 | } 78 | } 79 | ) 80 | } else { 81 | Logger.getLogger(TAG) 82 | .log(Level.INFO, "RTM user already logged in") 83 | } 84 | } 85 | 86 | /** 87 | * Function to create a RTM channel 88 | */ 89 | fun createRtmChannel() { 90 | try { 91 | this.hostView.connectionData.rtmChannelName = this.hostView.connectionData.rtmChannelName 92 | ?.let { this.hostView.connectionData.rtmChannelName } 93 | ?: let { this.hostView.connectionData.channel } 94 | 95 | this.hostView.agRtmChannel = 96 | this.hostView.agRtmClient.createChannel( 97 | this.hostView.connectionData.rtmChannelName, 98 | this.hostView.agoraRtmChannelHandler 99 | ) 100 | } catch (e: RuntimeException) { 101 | Logger.getLogger(TAG).log(Level.SEVERE, "Failed to create RTM channel. Error: $e") 102 | } 103 | 104 | if (hostView.isAgRtmChannelInitialized()) { 105 | joinRtmChannel() 106 | } 107 | } 108 | 109 | /** 110 | * Function to join a RTM channel 111 | */ 112 | private fun joinRtmChannel() { 113 | this.hostView.agRtmChannel.join(object : ResultCallback { 114 | override fun onSuccess(responseInfo: Void?) { 115 | isInRtmChannel = true 116 | Logger.getLogger(TAG).log(Level.SEVERE, "RTM Channel Joined Successfully") 117 | if (isInRtmChannel) { 118 | sendUserData(toChannel = true, hostView = hostView) 119 | } 120 | } 121 | 122 | override fun onFailure(errorInfo: ErrorInfo) { 123 | isInRtmChannel = false 124 | Logger.getLogger(TAG) 125 | .log(Level.SEVERE, "Failed to join RTM Channel. Error: $errorInfo") 126 | } 127 | }) 128 | } 129 | 130 | /** 131 | * Function to generate a random RTM ID if not specified by the user 132 | */ 133 | fun generateRtmId() { 134 | val charPool: List = ('a'..'z') + ('A'..'Z') + ('0'..'9') 135 | 136 | generatedRtmId = (1..10) 137 | .map { _ -> kotlin.random.Random.nextInt(0, charPool.size) } 138 | .map(charPool::get) 139 | .joinToString("") 140 | 141 | Logger.getLogger(TAG).log(Level.INFO, "Generated RTM ID: $generatedRtmId") 142 | 143 | this.hostView.connectionData.rtmId = generatedRtmId 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraVideoViewer+Buttons.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.graphics.Color 6 | import android.view.Gravity 7 | import android.view.View 8 | import android.view.ViewGroup 9 | import android.widget.FrameLayout 10 | import android.widget.LinearLayout 11 | 12 | internal class ButtonContainer(context: Context) : LinearLayout(context) 13 | 14 | @ExperimentalUnsignedTypes 15 | internal fun AgoraVideoViewer.getControlContainer(): ButtonContainer { 16 | this.controlContainer?.let { 17 | return it 18 | } 19 | val container = ButtonContainer(context) 20 | container.visibility = View.VISIBLE 21 | container.gravity = Gravity.CENTER 22 | val containerLayout = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, 200, Gravity.BOTTOM) 23 | 24 | this.addView(container, containerLayout) 25 | 26 | this.controlContainer = container 27 | return container 28 | } 29 | 30 | @ExperimentalUnsignedTypes 31 | internal fun AgoraVideoViewer.getCameraButton(): AgoraButton { 32 | this.camButton?.let { 33 | return it 34 | } 35 | val agCamButton = AgoraButton(context = this.context) 36 | agCamButton.clickAction = { 37 | (this.context as Activity).runOnUiThread { 38 | it.isSelected = !it.isSelected 39 | it.background.setTint(if (it.isSelected) Color.RED else Color.GRAY) 40 | this.agkit.enableLocalVideo(!it.isSelected) 41 | } 42 | } 43 | this.camButton = agCamButton 44 | agCamButton.setImageResource(R.drawable.ic_video_mute) 45 | return agCamButton 46 | } 47 | 48 | @ExperimentalUnsignedTypes 49 | internal fun AgoraVideoViewer.getMicButton(): AgoraButton { 50 | this.micButton?.let { 51 | return it 52 | } 53 | val agMicButton = AgoraButton(context = this.context) 54 | agMicButton.clickAction = { 55 | it.isSelected = !it.isSelected 56 | it.background.setTint(if (it.isSelected) Color.RED else Color.GRAY) 57 | this.userVideoLookup[this.userID]?.audioMuted = it.isSelected 58 | this.agkit.muteLocalAudioStream(it.isSelected) 59 | } 60 | this.micButton = agMicButton 61 | agMicButton.setImageResource(android.R.drawable.stat_notify_call_mute) 62 | return agMicButton 63 | } 64 | @ExperimentalUnsignedTypes 65 | internal fun AgoraVideoViewer.getFlipButton(): AgoraButton { 66 | this.flipButton?.let { 67 | return it 68 | } 69 | val agFlipButton = AgoraButton(context = this.context) 70 | agFlipButton.clickAction = { 71 | this.agkit.switchCamera() 72 | } 73 | this.flipButton = agFlipButton 74 | agFlipButton.setImageResource(R.drawable.btn_switch_camera) 75 | return agFlipButton 76 | } 77 | @ExperimentalUnsignedTypes 78 | internal fun AgoraVideoViewer.getEndCallButton(): AgoraButton { 79 | this.endCallButton?.let { 80 | return it 81 | } 82 | val hangupButton = AgoraButton(this.context) 83 | hangupButton.clickAction = { 84 | this.agkit.stopPreview() 85 | this.leaveChannel() 86 | } 87 | hangupButton.setImageResource(android.R.drawable.ic_menu_close_clear_cancel) 88 | hangupButton.background.setTint(Color.RED) 89 | this.endCallButton = hangupButton 90 | return hangupButton 91 | } 92 | 93 | @ExperimentalUnsignedTypes 94 | internal fun AgoraVideoViewer.getScreenShareButton(): AgoraButton? { 95 | return null 96 | } 97 | 98 | @ExperimentalUnsignedTypes 99 | internal fun AgoraVideoViewer.builtinButtons(): MutableList { 100 | val rtnButtons = mutableListOf() 101 | for (button in this.agoraSettings.enabledButtons) { 102 | rtnButtons += when (button) { 103 | AgoraSettings.BuiltinButton.MIC -> this.getMicButton() 104 | AgoraSettings.BuiltinButton.CAMERA -> this.getCameraButton() 105 | AgoraSettings.BuiltinButton.FLIP -> this.getFlipButton() 106 | AgoraSettings.BuiltinButton.END -> this.getEndCallButton() 107 | } 108 | } 109 | return rtnButtons 110 | } 111 | 112 | @ExperimentalUnsignedTypes 113 | internal fun AgoraVideoViewer.addVideoButtons() { 114 | val container = this.getControlContainer() 115 | val buttons = this.builtinButtons() + this.agoraSettings.extraButtons 116 | container.visibility = if (buttons.isEmpty()) View.INVISIBLE else View.VISIBLE 117 | 118 | val buttonSize = 100 119 | val buttonMargin = 10f 120 | buttons.forEach { button -> 121 | val llayout = LinearLayout.LayoutParams(buttonSize, buttonSize) 122 | llayout.gravity = Gravity.CENTER 123 | container.addView(button, llayout) 124 | } 125 | val contWidth = (buttons.size.toFloat() + buttonMargin) * buttons.count() 126 | this.positionButtonContainer(container, contWidth, buttonMargin) 127 | } 128 | 129 | @ExperimentalUnsignedTypes 130 | private fun AgoraVideoViewer.positionButtonContainer(container: ButtonContainer, contWidth: Float, buttonMargin: Float) { 131 | // TODO: Set container position and size 132 | 133 | container.setBackgroundColor(this.agoraSettings.colors.buttonBackgroundColor) 134 | container.background.alpha = this.agoraSettings.colors.buttonBackgroundAlpha 135 | // (container.subBtnContainer.layoutParams as? FrameLayout.LayoutParams)!!.width = contWidth.toInt() 136 | (this.backgroundVideoHolder.layoutParams as? ViewGroup.MarginLayoutParams) 137 | ?.bottomMargin = if (container.visibility == View.VISIBLE) container.measuredHeight else 0 138 | // this.addView(container) 139 | } 140 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | devrel@agora.io. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraRtmController/AgoraRtmController+MuteRequest.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android.AgoraRtmController 2 | 3 | import io.agora.agorauikit_android.AgoraVideoViewer 4 | import io.agora.agorauikit_android.R 5 | import io.agora.rtc2.RtcEngine 6 | import io.agora.rtm.ErrorInfo 7 | import io.agora.rtm.ResultCallback 8 | import io.agora.rtm.RtmClient 9 | import io.agora.rtm.RtmMessage 10 | import io.agora.rtm.SendMessageOptions 11 | import kotlinx.serialization.SerialName 12 | import kotlinx.serialization.Serializable 13 | import kotlinx.serialization.encodeToString 14 | import kotlinx.serialization.json.Json 15 | import java.util.logging.Level 16 | import java.util.logging.Logger 17 | 18 | @Serializable 19 | enum class DeviceType(val raw: Int) { 20 | CAMERA(0), MIC(1); 21 | companion object { 22 | fun fromInt(value: Int) = DeviceType.values().first { it.raw == value } 23 | } 24 | } 25 | 26 | @Serializable 27 | data class UserData( 28 | @SerialName("messageType") var messageType: String = "UserData", 29 | @SerialName("rtmId") var rtmId: String, 30 | @SerialName("rtcId") var rtcId: Int?, 31 | @SerialName("username") var username: String? = null, 32 | @SerialName("role") var role: Int, 33 | @SerialName("agora") var agoraVersion: AgoraVersion, 34 | @SerialName("uikit") var uiKitData: UIKitData, 35 | ) : java.io.Serializable 36 | 37 | @Serializable 38 | data class AgoraVersion( 39 | @SerialName("rtm") var rtmVersion: String, 40 | @SerialName("rtc") var rtcVersion: String, 41 | ) : java.io.Serializable { 42 | companion object { 43 | val current: AgoraVersion = AgoraVersion(RtmClient.getSdkVersion(), RtcEngine.getSdkVersion()) 44 | } 45 | } 46 | 47 | @Serializable 48 | data class UIKitData( 49 | @SerialName("platform") var platform: String, 50 | @SerialName("framework") var framework: String, 51 | @SerialName("version") var version: String, 52 | ) : java.io.Serializable { 53 | companion object { 54 | val current: UIKitData = UIKitData("android", "native", "4.0.2") 55 | } 56 | } 57 | 58 | @Serializable 59 | data class MuteRequest( 60 | @SerialName("messageType") var messageType: String = "MuteRequest", 61 | @SerialName("rtcId") var rtcId: Int, 62 | @SerialName("mute") var mute: Boolean, 63 | @SerialName("device") var device: Int, 64 | @SerialName("isForceful") var isForceful: Boolean, 65 | ) : java.io.Serializable 66 | 67 | @ExperimentalUnsignedTypes 68 | fun AgoraRtmController.Companion.sendUserData( 69 | toChannel: Boolean, 70 | peerRtmId: String? = null, 71 | hostView: AgoraVideoViewer 72 | ) { 73 | val TAG = hostView.resources.getString(R.string.TAG) 74 | 75 | val rtmId = hostView.connectionData.rtmId as String 76 | 77 | val userData = UserData( 78 | rtmId = rtmId, 79 | rtcId = hostView.userID, 80 | username = hostView.connectionData.username, 81 | role = hostView.userRole, 82 | agoraVersion = AgoraVersion.current, 83 | uiKitData = UIKitData.current 84 | ) 85 | 86 | val json = Json { encodeDefaults = true } 87 | val data = json.encodeToString(userData) 88 | val message: RtmMessage = hostView.agRtmClient.createMessage(data) 89 | 90 | Logger.getLogger(TAG).log(Level.INFO, message.text) 91 | 92 | val option = SendMessageOptions() 93 | 94 | if (!toChannel) { 95 | hostView.agRtmClient.sendMessageToPeer( 96 | peerRtmId, 97 | message, 98 | option, 99 | object : ResultCallback { 100 | override fun onSuccess(p0: Void?) { 101 | Logger.getLogger(TAG).log(Level.INFO, "UserData message sent to $peerRtmId") 102 | } 103 | 104 | override fun onFailure(p0: ErrorInfo?) { 105 | Logger.getLogger(TAG).log(Level.INFO, "Failed to send UserData message to $peerRtmId") 106 | } 107 | } 108 | ) 109 | } else { 110 | hostView.agRtmChannel.sendMessage( 111 | message, option, 112 | object : ResultCallback { 113 | override fun onSuccess(p0: Void?) { 114 | Logger.getLogger(TAG).log(Level.INFO, "UserData message sent to channel") 115 | } 116 | 117 | override fun onFailure(p0: ErrorInfo?) { 118 | Logger.getLogger(TAG).log(Level.INFO, "Failed to send UserData message to channel") 119 | } 120 | } 121 | ) 122 | } 123 | } 124 | 125 | @ExperimentalUnsignedTypes 126 | fun AgoraRtmController.Companion.sendMuteRequest( 127 | peerRtcId: Int, 128 | mute: Boolean, 129 | hostView: AgoraVideoViewer, 130 | deviceType: DeviceType, 131 | isForceful: Boolean = false 132 | ) { 133 | val TAG = hostView.resources.getString(R.string.TAG) 134 | 135 | var peerRtmId: String? 136 | 137 | val muteRequest = MuteRequest( 138 | rtcId = peerRtcId, 139 | mute = mute, 140 | device = deviceType.raw, 141 | isForceful = isForceful 142 | ) 143 | 144 | val json = Json { encodeDefaults = true } 145 | val data = json.encodeToString(muteRequest) 146 | val message: RtmMessage = hostView.agRtmClient.createMessage(data) 147 | 148 | val option = SendMessageOptions() 149 | 150 | if (peerRtcId == hostView.userID) { 151 | Logger.getLogger(TAG).log(Level.SEVERE, "Can't send message to local user") 152 | } else { 153 | if (hostView.agoraSettings.uidToUserIdMap.containsKey(peerRtcId)) { 154 | peerRtmId = hostView.agoraSettings.uidToUserIdMap.getValue(peerRtcId) 155 | 156 | hostView.agRtmClient.sendMessageToPeer( 157 | peerRtmId, 158 | message, 159 | option, 160 | object : ResultCallback { 161 | override fun onSuccess(p0: Void?) { 162 | Logger.getLogger(TAG).log(Level.INFO, "Mute Request sent to $peerRtmId") 163 | } 164 | 165 | override fun onFailure(p0: ErrorInfo?) { 166 | Logger.getLogger(TAG).log(Level.INFO, "Failed to send Mute Request to $peerRtmId. Error $p0") 167 | } 168 | } 169 | ) 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # 4 | # Copyright 2015 the original author or authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | ## 21 | ## Gradle start up script for UN*X 22 | ## 23 | ############################################################################## 24 | 25 | # Attempt to set APP_HOME 26 | # Resolve links: $0 may be a link 27 | PRG="$0" 28 | # Need this for relative symlinks. 29 | while [ -h "$PRG" ] ; do 30 | ls=`ls -ld "$PRG"` 31 | link=`expr "$ls" : '.*-> \(.*\)$'` 32 | if expr "$link" : '/.*' > /dev/null; then 33 | PRG="$link" 34 | else 35 | PRG=`dirname "$PRG"`"/$link" 36 | fi 37 | done 38 | SAVED="`pwd`" 39 | cd "`dirname \"$PRG\"`/" >/dev/null 40 | APP_HOME="`pwd -P`" 41 | cd "$SAVED" >/dev/null 42 | 43 | APP_NAME="Gradle" 44 | APP_BASE_NAME=`basename "$0"` 45 | 46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 48 | 49 | # Use the maximum available, or set MAX_FD != -1 to use that value. 50 | MAX_FD="maximum" 51 | 52 | warn () { 53 | echo "$*" 54 | } 55 | 56 | die () { 57 | echo 58 | echo "$*" 59 | echo 60 | exit 1 61 | } 62 | 63 | # OS specific support (must be 'true' or 'false'). 64 | cygwin=false 65 | msys=false 66 | darwin=false 67 | nonstop=false 68 | case "`uname`" in 69 | CYGWIN* ) 70 | cygwin=true 71 | ;; 72 | Darwin* ) 73 | darwin=true 74 | ;; 75 | MINGW* ) 76 | msys=true 77 | ;; 78 | NONSTOP* ) 79 | nonstop=true 80 | ;; 81 | esac 82 | 83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 84 | 85 | 86 | # Determine the Java command to use to start the JVM. 87 | if [ -n "$JAVA_HOME" ] ; then 88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 89 | # IBM's JDK on AIX uses strange locations for the executables 90 | JAVACMD="$JAVA_HOME/jre/sh/java" 91 | else 92 | JAVACMD="$JAVA_HOME/bin/java" 93 | fi 94 | if [ ! -x "$JAVACMD" ] ; then 95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 96 | 97 | Please set the JAVA_HOME variable in your environment to match the 98 | location of your Java installation." 99 | fi 100 | else 101 | JAVACMD="java" 102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 103 | 104 | Please set the JAVA_HOME variable in your environment to match the 105 | location of your Java installation." 106 | fi 107 | 108 | # Increase the maximum file descriptors if we can. 109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 110 | MAX_FD_LIMIT=`ulimit -H -n` 111 | if [ $? -eq 0 ] ; then 112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 113 | MAX_FD="$MAX_FD_LIMIT" 114 | fi 115 | ulimit -n $MAX_FD 116 | if [ $? -ne 0 ] ; then 117 | warn "Could not set maximum file descriptor limit: $MAX_FD" 118 | fi 119 | else 120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 121 | fi 122 | fi 123 | 124 | # For Darwin, add options to specify how the application appears in the dock 125 | if $darwin; then 126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 127 | fi 128 | 129 | # For Cygwin or MSYS, switch paths to Windows format before running java 130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then 131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 133 | 134 | JAVACMD=`cygpath --unix "$JAVACMD"` 135 | 136 | # We build the pattern for arguments to be converted via cygpath 137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 138 | SEP="" 139 | for dir in $ROOTDIRSRAW ; do 140 | ROOTDIRS="$ROOTDIRS$SEP$dir" 141 | SEP="|" 142 | done 143 | OURCYGPATTERN="(^($ROOTDIRS))" 144 | # Add a user-defined pattern to the cygpath arguments 145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 147 | fi 148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 149 | i=0 150 | for arg in "$@" ; do 151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 153 | 154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 156 | else 157 | eval `echo args$i`="\"$arg\"" 158 | fi 159 | i=`expr $i + 1` 160 | done 161 | case $i in 162 | 0) set -- ;; 163 | 1) set -- "$args0" ;; 164 | 2) set -- "$args0" "$args1" ;; 165 | 3) set -- "$args0" "$args1" "$args2" ;; 166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;; 167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 172 | esac 173 | fi 174 | 175 | # Escape application args 176 | save () { 177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 178 | echo " " 179 | } 180 | APP_ARGS=`save "$@"` 181 | 182 | # Collect all arguments for the java command, following the shell quoting and substitution rules 183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 184 | 185 | exec "$JAVACMD" "$@" 186 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraVideoViewer+Ordering.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import android.graphics.Color 4 | import android.view.Gravity 5 | import android.view.View 6 | import android.view.ViewGroup 7 | import android.widget.FrameLayout 8 | import android.widget.ImageView 9 | import android.widget.LinearLayout 10 | import androidx.recyclerview.widget.GridLayoutManager 11 | import androidx.recyclerview.widget.RecyclerView 12 | import io.agora.rtc2.Constants 13 | import io.agora.rtc2.RtcEngine 14 | import kotlin.math.ceil 15 | import kotlin.math.max 16 | import kotlin.math.sqrt 17 | 18 | /** 19 | * Shuffle around the videos depending on the style 20 | */ 21 | @ExperimentalUnsignedTypes 22 | fun AgoraVideoViewer.reorganiseVideos() { 23 | this.organiseRecycleFloating() 24 | (this.backgroundVideoHolder.layoutParams as? ViewGroup.MarginLayoutParams) 25 | ?.topMargin = 26 | if (this.floatingVideoHolder.visibility == View.VISIBLE) this.floatingVideoHolder.measuredHeight else 0 27 | this.controlContainer?.let { 28 | (this.backgroundVideoHolder.layoutParams as? ViewGroup.MarginLayoutParams) 29 | ?.bottomMargin = if (it.visibility == View.VISIBLE) it.measuredHeight else 0 30 | } 31 | this.organiseRecycleGrid() 32 | } 33 | 34 | /** 35 | * Update the contents of the floating view 36 | */ 37 | @ExperimentalUnsignedTypes 38 | fun AgoraVideoViewer.organiseRecycleFloating() { 39 | val gridList = this.collectionViewVideos.keys.toList() 40 | this.floatingVideoHolder.visibility = if (gridList.isEmpty()) View.INVISIBLE else View.VISIBLE 41 | if (this.floatingVideoHolder.adapter == null) { 42 | val remoteViewManager = GridLayoutManager(context, 1, GridLayoutManager.HORIZONTAL, false) 43 | val remoteViewAdapter = FloatingViewAdapter(gridList, this) 44 | 45 | this.floatingVideoHolder.apply { 46 | layoutManager = remoteViewManager 47 | adapter = remoteViewAdapter 48 | // setHasFixedSize(true) 49 | } 50 | } else { 51 | (this.floatingVideoHolder.adapter as FloatingViewAdapter).uidList = gridList 52 | this.floatingVideoHolder.adapter?.notifyDataSetChanged() 53 | } 54 | } 55 | 56 | /** 57 | * Update the contents of the main grid view 58 | */ 59 | @ExperimentalUnsignedTypes 60 | fun AgoraVideoViewer.organiseRecycleGrid() { 61 | val gridList = this.userVideosForGrid.keys.toList() 62 | val maxSqrt = max(1f, ceil(sqrt(gridList.count().toFloat()))) 63 | 64 | if (this.backgroundVideoHolder.adapter == null) { 65 | val remoteViewManager = GridLayoutManager( 66 | context, 67 | max(maxSqrt.toInt(), 1), 68 | GridLayoutManager.VERTICAL, 69 | false 70 | ) 71 | val remoteViewAdapter = GridViewAdapter(gridList, this) 72 | 73 | this.backgroundVideoHolder.apply { 74 | layoutManager = remoteViewManager 75 | adapter = remoteViewAdapter 76 | // setHasFixedSize(true) 77 | } 78 | } else { 79 | (this.backgroundVideoHolder.adapter as GridViewAdapter).uidList = gridList 80 | (this.backgroundVideoHolder.layoutManager as? GridLayoutManager)?.spanCount = 81 | if (gridList.count() == 2) 1 else maxSqrt.toInt() 82 | this.backgroundVideoHolder.adapter?.notifyDataSetChanged() 83 | } 84 | } 85 | 86 | @ExperimentalUnsignedTypes 87 | internal class GridViewAdapter(var uidList: List, private val agoraVC: AgoraVideoViewer) : 88 | RecyclerView.Adapter() { 89 | class RemoteViewHolder(val frame: FrameLayout) : RecyclerView.ViewHolder(frame) 90 | 91 | val maxSqrt: Float 92 | get() = max(1f, ceil(sqrt(uidList.count().toFloat()))) 93 | val mRtcEngine: RtcEngine 94 | get() = this.agoraVC.agkit 95 | 96 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RemoteViewHolder { 97 | val remoteFrame = FrameLayout(parent.context) 98 | 99 | // The width of the FrameLayout is set to half the parent's width. 100 | // This is to make sure that the Grid has 2 columns 101 | remoteFrame.layoutParams = RecyclerView.LayoutParams( 102 | RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.MATCH_PARENT 103 | ) 104 | return RemoteViewHolder(remoteFrame) 105 | } 106 | 107 | override fun onBindViewHolder(holder: RemoteViewHolder, position: Int) { 108 | 109 | // First we unmute the remote video stream so that Agora can start fetching the remote video feed 110 | // We have to do this since we mute the remote video in the onUserJoined callback to save on bandwidth 111 | val uid = uidList[position] 112 | val videoView = agoraVC.userVideoLookup[uidList[position]] 113 | 114 | // We are tagging the SurfaceView object with the UID. 115 | // This keeps us from manually maintaining a mapping between the SurfaceView and UID 116 | // We'll see it used in the onViewRecycled method 117 | if (agoraVC.userID != uid) { 118 | if (agoraVC.agoraSettings.usingDualStream) { 119 | mRtcEngine.setRemoteVideoStreamType( 120 | uidList[position], 121 | if (this.itemCount < agoraVC.agoraSettings.gridThresholdHighBitrate) Constants.VIDEO_STREAM_HIGH else Constants.VIDEO_STREAM_LOW 122 | ) 123 | } 124 | // mRtcEngine.muteRemoteVideoStream(uidList[position], false) 125 | // We will now use Agora's setupRemoteVideo method to render the remote video stream on the SurfaceView 126 | // mRtcEngine.setupRemoteVideo(videoView!!.canvas) 127 | } else { 128 | // mRtcEngine.setupLocalVideo(videoView!!.canvas) 129 | } 130 | // videoView.visibility = View.INVISIBLE 131 | 132 | // videoView.parent 133 | // We'll add the SurfaceView as a child to the FrameLayout which is actually the ViewHolder in our RecyclerView 134 | (videoView?.parent as? FrameLayout)?.removeView(videoView) 135 | holder.frame.addView(videoView) 136 | (holder.frame.layoutParams as? RecyclerView.LayoutParams)?.height = 137 | agoraVC.backgroundVideoHolder.measuredHeight / maxSqrt.toInt() 138 | } 139 | 140 | override fun onViewRecycled(holder: RemoteViewHolder) { 141 | // We are calling this method when our view is removed from the RecyclerView Pool. 142 | // This allows us to save on bandwidth 143 | 144 | // We get the UID from the tag of the SurfaceView 145 | val agoraVideoView = holder.frame.getChildAt(0) as AgoraSingleVideoView 146 | holder.frame.removeView(agoraVideoView) 147 | // We mute the remote video stream of the UID 148 | } 149 | 150 | override fun getItemCount() = uidList.size 151 | } 152 | 153 | @ExperimentalUnsignedTypes 154 | internal class FloatingViewAdapter(var uidList: List, private val agoraVC: AgoraVideoViewer) : 155 | RecyclerView.Adapter() { 156 | class RemoteViewHolder(val frame: FrameLayout) : RecyclerView.ViewHolder(frame) 157 | 158 | val maxSqrt: Float 159 | get() = max(1f, ceil(sqrt(uidList.count().toFloat()))) 160 | val mRtcEngine: RtcEngine 161 | get() = this.agoraVC.agkit 162 | 163 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RemoteViewHolder { 164 | val linearLayout = LinearLayout(parent.context) 165 | val pinIcon = ImageView(parent.context) 166 | pinIcon.setImageResource(R.drawable.baseline_push_pin_20) 167 | pinIcon.layoutParams = ViewGroup.LayoutParams(100, 100) 168 | linearLayout.addView(pinIcon) 169 | linearLayout.gravity = Gravity.CENTER 170 | 171 | val remoteFrame = FrameLayout(parent.context) 172 | // The width of the FrameLayout is set to half the parent's width. 173 | // This is to make sure that the Grid has 2 columns 174 | val recycleParams = RecyclerView.LayoutParams(190, 190) 175 | recycleParams.setMargins(5, 5, 5, 5) 176 | remoteFrame.layoutParams = recycleParams 177 | remoteFrame.setBackgroundColor(Color.BLUE) 178 | remoteFrame.addView(linearLayout) 179 | return RemoteViewHolder(remoteFrame) 180 | } 181 | 182 | override fun onBindViewHolder(holder: RemoteViewHolder, position: Int) { 183 | 184 | // First we unmute the remote video stream so that Agora can start fetching the remote video feed 185 | // We have to do this since we mute the remote video in the onUserJoined callback to save on bandwidth 186 | val uid = uidList[position] 187 | val videoView = agoraVC.userVideoLookup[uidList[position]] 188 | val audioMuted = agoraVC.userVideoLookup[uidList[position]]?.audioMuted 189 | val videoMuted = agoraVC.userVideoLookup[uidList[position]]?.videoMuted 190 | val activeSpeaker = 191 | this.agoraVC.overrideActiveSpeaker ?: this.agoraVC.activeSpeaker ?: this.agoraVC.userID 192 | if (activeSpeaker == uid) { 193 | return 194 | } 195 | // CreateRendererView is used to create a SurfaceView object 196 | // val surface = RtcEngine.CreateRendererView(holder.itemView.context) 197 | 198 | // We are tagging the SurfaceView object with the UID. 199 | // This keeps us from manually maintaining a mapping between the SurfaceView and UID 200 | // We'll see it used in the onViewRecycled method 201 | // videoView.tag = uidList[position] 202 | 203 | // videoView.parent 204 | // We'll add the SurfaceView as a child to the FrameLayout which is actually the ViewHolder in our RecyclerView 205 | (videoView?.parent as? FrameLayout)?.removeView(videoView) 206 | holder.frame.addView(videoView) 207 | if (agoraVC.userID != uid) { 208 | if (agoraVC.agoraSettings.usingDualStream) { 209 | mRtcEngine.setRemoteVideoStreamType(uidList[position], Constants.VIDEO_STREAM_LOW) 210 | } 211 | mRtcEngine.muteRemoteVideoStream(uidList[position], false) 212 | // We will now use Agora's setupRemoteVideo method to render the remote video stream on the SurfaceView 213 | mRtcEngine.setupRemoteVideo(videoView!!.canvas) 214 | } else { 215 | mRtcEngine.setupLocalVideo(videoView!!.canvas) 216 | } 217 | 218 | holder.itemView.setOnClickListener { 219 | val newID = if (videoView.uid == 0) this.agoraVC.userID else videoView.uid 220 | if (this.agoraVC.overrideActiveSpeaker == newID) { 221 | this.agoraVC.overrideActiveSpeaker = null 222 | } else { 223 | this.agoraVC.overrideActiveSpeaker = newID 224 | } 225 | } 226 | 227 | // (holder.frame.layoutParams as RecyclerView.LayoutParams).height = agoraVC.measuredHeight / maxSqrt.toInt() 228 | } 229 | 230 | override fun onViewRecycled(holder: RemoteViewHolder) { 231 | // We are calling this method when our view is removed from the RecyclerView Pool. 232 | // This allows us to save on bandwidth 233 | // We get the UID from the tag of the SurfaceVi ew 234 | (holder.frame.getChildAt(0) as? AgoraSingleVideoView)?.let { 235 | holder.frame.removeView(it) 236 | } 237 | // (agoraVideoView.layoutParams as FrameLayout.LayoutParams).width = 238 | // We mute the remote video stream of the UID 239 | // mRtcEngine.muteRemoteVideoStream(uid, false) 240 | } 241 | 242 | override fun getItemCount() = uidList.size 243 | } 244 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraVideoViewer.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.content.res.Resources 6 | import android.graphics.Color 7 | import android.view.Gravity 8 | import android.widget.FrameLayout 9 | import android.widget.ImageView 10 | import android.widget.PopupMenu 11 | import androidx.constraintlayout.widget.ConstraintLayout 12 | import androidx.recyclerview.widget.RecyclerView 13 | import io.agora.agorauikit_android.AgoraRtmController.AgoraRtmChannelHandler 14 | import io.agora.agorauikit_android.AgoraRtmController.AgoraRtmClientHandler 15 | import io.agora.agorauikit_android.AgoraRtmController.AgoraRtmController 16 | import io.agora.agorauikit_android.AgoraRtmController.DeviceType 17 | import io.agora.agorauikit_android.AgoraRtmController.RtmTokenCallback 18 | import io.agora.agorauikit_android.AgoraRtmController.RtmTokenError 19 | import io.agora.agorauikit_android.AgoraRtmController.fetchToken 20 | import io.agora.agorauikit_android.AgoraRtmController.sendMuteRequest 21 | import io.agora.rtc2.Constants 22 | import io.agora.rtc2.IRtcEngineEventHandler 23 | import io.agora.rtc2.RtcEngine 24 | import io.agora.rtc2.RtcEngineConfig 25 | import io.agora.rtc2.video.BeautyOptions 26 | import io.agora.rtc2.video.VideoEncoderConfiguration 27 | import io.agora.rtm.RtmChannel 28 | import io.agora.rtm.RtmClient 29 | import java.util.logging.Level 30 | import java.util.logging.Logger 31 | 32 | /** 33 | * An interface for getting some common delegate callbacks without needing to subclass. 34 | */ 35 | interface AgoraVideoViewerDelegate { 36 | /** 37 | * Local user has joined a channel 38 | * @param channel Channel that the local user has joined. 39 | */ 40 | fun joinedChannel(channel: String) {} 41 | 42 | /** 43 | * Local user has left a channel 44 | * @param channel Channel that the local user has left. 45 | */ 46 | fun leftChannel(channel: String) {} 47 | 48 | /** 49 | * The token used to connect to the current active channel will expire in 30 seconds. 50 | * @param token Token that is currently used to connect to the channel. 51 | * @return Return true if the token fetch is being handled by this method. 52 | */ 53 | fun tokenWillExpire(token: String?): Boolean { 54 | return false 55 | } 56 | 57 | /** 58 | * The token used to connect to the current active channel has expired. 59 | * @return Return true if the token fetch is being handled by this method. 60 | */ 61 | fun tokenDidExpire(): Boolean { 62 | return false 63 | } 64 | } 65 | 66 | @ExperimentalUnsignedTypes 67 | 68 | /** 69 | * View to contain all the video session objects, including camera feeds and buttons for settings 70 | */ 71 | open class AgoraVideoViewer : FrameLayout { 72 | 73 | val TAG = resources.getString(R.string.TAG) 74 | 75 | /** 76 | * Style and organisation to be applied to all the videos in this view. 77 | */ 78 | enum class Style { 79 | GRID, FLOATING, COLLECTION 80 | } 81 | 82 | /** 83 | * Gets and sets the role for the user. Either `.audience` or `.broadcaster`. 84 | */ 85 | var userRole: Int = Constants.CLIENT_ROLE_BROADCASTER 86 | set(value: Int) { 87 | field = value 88 | this.agkit.setClientRole(value) 89 | } 90 | 91 | internal var controlContainer: ButtonContainer? = null 92 | internal var camButton: AgoraButton? = null 93 | internal var micButton: AgoraButton? = null 94 | internal var flipButton: AgoraButton? = null 95 | internal var endCallButton: AgoraButton? = null 96 | internal var screenShareButton: AgoraButton? = null 97 | 98 | companion object {} 99 | 100 | internal var remoteUserIDs: MutableSet = mutableSetOf() 101 | internal var userVideoLookup: MutableMap = mutableMapOf() 102 | internal val userVideosForGrid: Map 103 | get() { 104 | return if (this.style == Style.FLOATING) { 105 | this.userVideoLookup.filterKeys { 106 | it == (this.overrideActiveSpeaker ?: this.activeSpeaker ?: this.userID) 107 | } 108 | } else if (this.style == Style.GRID) { 109 | this.userVideoLookup 110 | } else { 111 | emptyMap() 112 | } 113 | } 114 | 115 | /** 116 | * Default beautification settings 117 | */ 118 | open val beautyOptions: BeautyOptions 119 | get() { 120 | val beautyOptions = BeautyOptions() 121 | beautyOptions.smoothnessLevel = 1f 122 | beautyOptions.rednessLevel = 0.1f 123 | return beautyOptions 124 | } 125 | 126 | /** 127 | * Video views to be displayed in the floating collection view. 128 | */ 129 | val collectionViewVideos: Map 130 | get() { 131 | return if (this.style == Style.FLOATING) { 132 | return this.userVideoLookup 133 | } else { 134 | emptyMap() 135 | } 136 | } 137 | 138 | /** 139 | * ID of the local user. 140 | * Setting to zero will tell Agora to assign one for you once connected. 141 | */ 142 | public var userID: Int = 0 143 | internal set 144 | 145 | /** 146 | * A boolean to check whether the user has joined the RTC channel or not. 147 | */ 148 | var isInRtcChannel: Boolean? = false 149 | 150 | /** 151 | * The most recently active speaker in the session. 152 | * This will only ever be set to remote users, not the local user. 153 | */ 154 | public var activeSpeaker: Int? = null 155 | internal set 156 | private val newHandler = AgoraVideoViewerHandler(this) 157 | internal val agoraRtmClientHandler = AgoraRtmClientHandler(this) 158 | internal val agoraRtmChannelHandler = AgoraRtmChannelHandler(this) 159 | 160 | var rtcOverrideHandler: IRtcEngineEventHandler? = null 161 | var rtmClientOverrideHandler: AgoraRtmClientHandler? = null 162 | var rtmChannelOverrideHandler: AgoraRtmChannelHandler? = null 163 | 164 | internal fun addUserVideo(userId: Int): AgoraSingleVideoView { 165 | this.userVideoLookup[userId]?.let { remoteView -> 166 | return remoteView 167 | } 168 | val remoteVideoView = 169 | AgoraSingleVideoView(this.context, userId, this.agoraSettings.colors.micFlag) 170 | remoteVideoView.canvas.renderMode = this.agoraSettings.videoRenderMode 171 | this.agkit.setupRemoteVideo(remoteVideoView.canvas) 172 | // this.agkit.setRemoteVideoRenderer(remoteVideoView.uid, remoteVideoView.textureView) 173 | this.userVideoLookup[userId] = remoteVideoView 174 | 175 | var hostControl: ImageView = ImageView(this.context) 176 | val density = Resources.getSystem().displayMetrics.density 177 | val hostControlLayout = FrameLayout.LayoutParams(40 * density.toInt(), 40 * density.toInt()) 178 | hostControlLayout.gravity = Gravity.END 179 | 180 | hostControl = ImageView(this.context) 181 | hostControl.setImageResource(R.drawable.ic_round_pending_24) 182 | hostControl.setColorFilter(Color.WHITE) 183 | hostControl.setOnClickListener { 184 | val menu = PopupMenu(this.context, remoteVideoView) 185 | 186 | menu.menu.apply { 187 | add("Request user to " + (if (remoteVideoView.audioMuted) "un" else "") + "mute the mic").setOnMenuItemClickListener { 188 | AgoraRtmController.Companion.sendMuteRequest( 189 | peerRtcId = userId, 190 | mute = !remoteVideoView.audioMuted, 191 | hostView = this@AgoraVideoViewer, 192 | deviceType = DeviceType.MIC 193 | ) 194 | true 195 | } 196 | add("Request user to " + (if (remoteVideoView.videoMuted) "en" else "dis") + "able the camera").setOnMenuItemClickListener { 197 | AgoraRtmController.Companion.sendMuteRequest( 198 | peerRtcId = userId, 199 | mute = !remoteVideoView.videoMuted, 200 | hostView = this@AgoraVideoViewer, 201 | deviceType = DeviceType.CAMERA 202 | ) 203 | true 204 | } 205 | } 206 | menu.show() 207 | } 208 | if (agoraSettings.rtmEnabled) { 209 | remoteVideoView.addView(hostControl, hostControlLayout) 210 | } 211 | 212 | if (this.activeSpeaker == null) { 213 | this.activeSpeaker = userId 214 | } 215 | this.reorganiseVideos() 216 | return remoteVideoView 217 | } 218 | 219 | internal fun removeUserVideo(uid: Int, reogranise: Boolean = true) { 220 | val userSingleView = this.userVideoLookup[uid] ?: return 221 | // val canView = userSingleView.hostingView ?: return 222 | this.agkit.muteRemoteVideoStream(uid, true) 223 | userSingleView.canvas.view = null 224 | this.userVideoLookup.remove(uid) 225 | 226 | this.activeSpeaker.let { 227 | if (it == uid) this.setRandomSpeaker() 228 | } 229 | if (reogranise) { 230 | this.reorganiseVideos() 231 | } 232 | } 233 | 234 | internal fun setRandomSpeaker() { 235 | this.activeSpeaker = this.userVideoLookup.keys.shuffled().firstOrNull { it != this.userID } 236 | } 237 | 238 | /** 239 | * Active speaker override. 240 | */ 241 | public var overrideActiveSpeaker: Int? = null 242 | set(newValue) { 243 | val oldValue = this.overrideActiveSpeaker 244 | field = newValue 245 | if (field != oldValue) { 246 | this.reorganiseVideos() 247 | } 248 | } 249 | 250 | internal fun addLocalVideo(): AgoraSingleVideoView? { 251 | if (this.userID == 0 || this.userVideoLookup.containsKey(this.userID)) { 252 | return this.userVideoLookup[this.userID] 253 | } 254 | this.agkit.enableVideo() 255 | this.agkit.startPreview() 256 | val vidView = AgoraSingleVideoView(this.context, 0, this.agoraSettings.colors.micFlag) 257 | vidView.canvas.renderMode = this.agoraSettings.videoRenderMode 258 | this.agkit.enableVideo() 259 | this.agkit.setupLocalVideo(vidView.canvas) 260 | this.agkit.startPreview() 261 | this.userVideoLookup[this.userID] = vidView 262 | this.reorganiseVideos() 263 | return vidView 264 | } 265 | 266 | internal var connectionData: AgoraConnectionData 267 | 268 | /** 269 | * Creates an AgoraVideoViewer object, to be placed anywhere in your application. 270 | * @param context: Application context 271 | * @param connectionData: Storing struct for holding data about the connection to Agora service. 272 | * @param style: Style and organisation to be applied to all the videos in this AgoraVideoViewer. 273 | * @param agoraSettings: Settings for this viewer. This can include style customisations and information of where to get new tokens from. 274 | * @param delegate: Delegate for the AgoraVideoViewer, used for some important callback methods. 275 | */ 276 | @Throws(Exception::class) 277 | @JvmOverloads public constructor( 278 | context: Context, 279 | connectionData: AgoraConnectionData, 280 | style: Style = Style.FLOATING, 281 | agoraSettings: AgoraSettings = AgoraSettings(), 282 | delegate: AgoraVideoViewerDelegate? = null 283 | ) : super(context) { 284 | this.connectionData = connectionData 285 | this.style = style 286 | this.agoraSettings = agoraSettings 287 | this.delegate = delegate 288 | // this.setBackgroundColor(Color.BLUE) 289 | initAgoraEngine() 290 | this.addView( 291 | this.backgroundVideoHolder, 292 | ConstraintLayout.LayoutParams( 293 | ConstraintLayout.LayoutParams.MATCH_PARENT, 294 | ConstraintLayout.LayoutParams.MATCH_PARENT 295 | ) 296 | ) 297 | this.addView( 298 | this.floatingVideoHolder, 299 | ConstraintLayout.LayoutParams(ConstraintLayout.LayoutParams.MATCH_PARENT, 200) 300 | ) 301 | this.floatingVideoHolder.setBackgroundColor(this.agoraSettings.colors.floatingBackgroundColor) 302 | this.floatingVideoHolder.background.alpha = 303 | this.agoraSettings.colors.floatingBackgroundAlpha 304 | } 305 | 306 | val agoraRtmController = AgoraRtmController(this) 307 | 308 | @Throws(Exception::class) 309 | private fun initAgoraEngine() { 310 | if (connectionData.appId == "my-app-id") { 311 | Logger.getLogger(TAG).log(Level.SEVERE, "Change the App ID!") 312 | throw IllegalArgumentException("Change the App ID!") 313 | } 314 | val rtcEngineConfig = RtcEngineConfig() 315 | rtcEngineConfig.mAppId = connectionData.appId 316 | rtcEngineConfig.mContext = context.applicationContext 317 | rtcEngineConfig.mEventHandler = this.newHandler 318 | 319 | try { 320 | this.agkit = RtcEngine.create(rtcEngineConfig) 321 | } catch (e: Exception) { 322 | println("Exception while initializing the SDK : ${e.message}") 323 | } 324 | 325 | agkit.setParameters("{\"rtc.using_ui_kit\": 1}") 326 | agkit.enableAudioVolumeIndication(1000, 3, true) 327 | agkit.setClientRole(this.userRole) 328 | agkit.enableVideo() 329 | agkit.setVideoEncoderConfiguration(VideoEncoderConfiguration()) 330 | if (agoraSettings.rtmEnabled) { 331 | agoraRtmController.initAgoraRtm(context) 332 | } 333 | } 334 | 335 | /** 336 | * Delegate for the AgoraVideoViewer, used for some important callback methods. 337 | */ 338 | public var delegate: AgoraVideoViewerDelegate? = null 339 | 340 | internal var floatingVideoHolder: RecyclerView = RecyclerView(context) 341 | internal var backgroundVideoHolder: RecyclerView = RecyclerView(context) 342 | 343 | /** 344 | * Settings and customisations such as position of on-screen buttons, collection view of all channel members, 345 | * as well as agora video configuration. 346 | */ 347 | public var agoraSettings: AgoraSettings = AgoraSettings() 348 | internal set 349 | 350 | /** 351 | * Style and organisation to be applied to all the videos in this AgoraVideoViewer. 352 | */ 353 | public var style: Style 354 | set(value: Style) { 355 | val oldValue = field 356 | field = value 357 | if (oldValue != value) { 358 | // this.backgroundVideoHolder.visibility = if (value == Style.COLLECTION) INVISIBLE else VISIBLE 359 | this.reorganiseVideos() 360 | } 361 | } 362 | 363 | /** 364 | * RtcEngine being used by this AgoraVideoViewer 365 | */ 366 | public lateinit var agkit: RtcEngine 367 | internal set 368 | 369 | /** 370 | * RTM client used by this [AgoraVideoViewer] 371 | */ 372 | public lateinit var agRtmClient: RtmClient 373 | internal set 374 | lateinit var agRtmChannel: RtmChannel 375 | internal set 376 | 377 | fun isAgRtmChannelInitialized() = ::agRtmChannel.isInitialized 378 | 379 | fun isAgRtmClientInitialized() = ::agRtmClient.isInitialized 380 | 381 | // VideoControl 382 | 383 | internal fun setupAgoraVideo() { 384 | if (this.agkit.enableVideo() < 0) { 385 | Logger.getLogger(TAG).log(Level.WARNING, "Could not enable video") 386 | return 387 | } 388 | if (this.controlContainer == null) { 389 | this.addVideoButtons() 390 | } 391 | this.agkit.setVideoEncoderConfiguration(this.agoraSettings.videoConfiguration) 392 | } 393 | 394 | /** 395 | * Leave channel stops all preview elements 396 | * @return Same return as RtcEngine.leaveChannel, 0 means no problem, less than 0 means there was an issue leaving 397 | */ 398 | fun leaveChannel(): Int { 399 | val channelName = this.connectionData.channel ?: return 0 400 | this.agkit.setupLocalVideo(null) 401 | if (this.userRole == Constants.CLIENT_ROLE_BROADCASTER) { 402 | this.agkit.stopPreview() 403 | } 404 | this.activeSpeaker = null 405 | (this.context as Activity).runOnUiThread { 406 | this.remoteUserIDs.forEach { this.removeUserVideo(it, false) } 407 | this.remoteUserIDs = mutableSetOf() 408 | this.userVideoLookup = mutableMapOf() 409 | this.reorganiseVideos() 410 | this.controlContainer?.visibility = INVISIBLE 411 | } 412 | 413 | val leaveChannelRtn = this.agkit.leaveChannel() 414 | if (leaveChannelRtn >= 0) { 415 | this.connectionData.channel = null 416 | this.delegate?.leftChannel(channelName) 417 | } 418 | return leaveChannelRtn 419 | } 420 | 421 | /** 422 | * Join the Agora channel with optional token request 423 | * @param channel: Channel name to join 424 | * @param fetchToken: Whether the token should be fetched before joining the channel. A token will only be fetched if a token URL is provided in AgoraSettings. 425 | * @param role: [AgoraClientRole](https://docs.agora.io/en/Video/API%20Reference/oc/Constants/AgoraClientRole.html) to join the channel as. Default: `.broadcaster` 426 | * @param uid: UID to be set when user joins the channel, default will be 0. 427 | */ 428 | @JvmOverloads fun join(channel: String, fetchToken: Boolean, role: Int? = null, uid: Int? = null) { 429 | this.setupAgoraVideo() 430 | getRtcToken(channel, role, uid, fetchToken) 431 | 432 | if (agoraSettings.rtmEnabled) { 433 | getRtmToken(fetchToken) 434 | } 435 | } 436 | 437 | private fun getRtcToken(channel: String, role: Int? = null, uid: Int? = null, fetchToken: Boolean) { 438 | if (fetchToken) { 439 | this.agoraSettings.tokenURL?.let { tokenURL -> 440 | AgoraVideoViewer.Companion.fetchToken( 441 | tokenURL, channel, uid ?: this.userID, 442 | object : TokenCallback { 443 | override fun onSuccess(token: String) { 444 | this@AgoraVideoViewer.connectionData.appToken = token 445 | this@AgoraVideoViewer.join(channel, token, role, uid) 446 | } 447 | 448 | override fun onError(error: TokenError) { 449 | Logger.getLogger(TAG, "Could not get RTC token: ${error.name}") 450 | } 451 | } 452 | ) 453 | } 454 | return 455 | } 456 | this.join(channel, this.connectionData.appToken, role, uid) 457 | } 458 | 459 | private fun getRtmToken(fetchToken: Boolean) { 460 | if (connectionData.rtmId.isNullOrEmpty()) { 461 | agoraRtmController.generateRtmId() 462 | } 463 | 464 | if (fetchToken) { 465 | this.agoraSettings.tokenURL?.let { tokenURL -> 466 | AgoraRtmController.Companion.fetchToken( 467 | tokenURL, 468 | rtmId = connectionData.rtmId as String, 469 | completion = object : RtmTokenCallback { 470 | override fun onSuccess(token: String) { 471 | connectionData.rtmToken = token 472 | } 473 | 474 | override fun onError(error: RtmTokenError) { 475 | Logger.getLogger(TAG, "Could not get RTM token: ${error.name}") 476 | } 477 | } 478 | ) 479 | } 480 | return 481 | } 482 | } 483 | 484 | /** 485 | * Login to Agora RTM 486 | */ 487 | fun triggerLoginToRtm() { 488 | if (agoraSettings.rtmEnabled && isAgRtmClientInitialized()) { 489 | agoraRtmController.loginToRtm() 490 | } else { 491 | Logger.getLogger(TAG) 492 | .log(Level.WARNING, "Username is null or RTM client has not been initialized") 493 | } 494 | } 495 | 496 | /** 497 | * Join the Agora channel with optional token request 498 | * @param channel: Channel name to join 499 | * @param token: token to be applied to the channel join. Leave null to use an existing token or no token. 500 | * @param role: [AgoraClientRole](https://docs.agora.io/en/Video/API%20Reference/oc/Constants/AgoraClientRole.html) to join the channel as. 501 | * @param uid: UID to be set when user joins the channel, default will be 0. 502 | */ 503 | @JvmOverloads fun join(channel: String, token: String? = null, role: Int? = null, uid: Int? = null) { 504 | 505 | if (role == Constants.CLIENT_ROLE_BROADCASTER) { 506 | AgoraVideoViewer.requestPermission(this.context) 507 | } 508 | if (this.connectionData.channel != null) { 509 | if (this.connectionData.channel == channel) { 510 | // already in this channel 511 | return 512 | } 513 | val leaveChannelRtn = this.leaveChannel() 514 | if (leaveChannelRtn < 0) { 515 | // could not leave channel 516 | Logger.getLogger(TAG) 517 | .log(Level.WARNING, "Could not leave channel: $leaveChannelRtn") 518 | } else { 519 | this.join(channel, token, role, uid) 520 | } 521 | return 522 | } 523 | role?.let { 524 | if (it != this.userRole) { 525 | this.userRole = it 526 | } 527 | } 528 | uid?.let { 529 | this.userID = it 530 | } 531 | 532 | this.setupAgoraVideo() 533 | this.agkit.joinChannel(token ?: this.agoraSettings.tokenURL, channel, null, this.userID) 534 | } 535 | } 536 | -------------------------------------------------------------------------------- /agorauikit_android/src/main/java/io/agora/agorauikit_android/AgoraVideoViewerHandler.kt: -------------------------------------------------------------------------------- 1 | package io.agora.agorauikit_android 2 | 3 | import android.app.Activity 4 | import android.graphics.Rect 5 | import io.agora.agorauikit_android.AgoraRtmController.AgoraRtmController 6 | import io.agora.rtc2.ClientRoleOptions 7 | import io.agora.rtc2.Constants 8 | import io.agora.rtc2.IRtcEngineEventHandler 9 | import io.agora.rtc2.UserInfo 10 | import java.util.logging.Level 11 | import java.util.logging.Logger 12 | 13 | /** 14 | * Class for all the Agora RTC event handlers 15 | * 16 | * @param hostView [AgoraVideoViewer] 17 | */ 18 | @ExperimentalUnsignedTypes 19 | class AgoraVideoViewerHandler(private val hostView: AgoraVideoViewer) : 20 | IRtcEngineEventHandler() { 21 | 22 | val TAG = this.hostView.resources.getString(R.string.TAG) 23 | 24 | override fun onClientRoleChanged(oldRole: Int, newRole: Int, newRoleOptions: ClientRoleOptions?) { 25 | super.onClientRoleChanged(oldRole, newRole, newRoleOptions) 26 | val isHost = newRole == Constants.CLIENT_ROLE_BROADCASTER 27 | if (!isHost) { 28 | this.hostView.userVideoLookup.remove(this.hostView.userID) 29 | } else if (!this.hostView.userVideoLookup.contains(this.hostView.userID)) { 30 | (this.hostView.context as Activity).runOnUiThread { 31 | this.hostView.addLocalVideo() 32 | } 33 | } 34 | // Only show the camera options when we are a broadcaster 35 | // this.getControlContainer().isHidden = !isHost 36 | 37 | this.hostView.rtcOverrideHandler?.onClientRoleChanged(oldRole, newRole, newRoleOptions) 38 | } 39 | 40 | override fun onUserJoined(uid: Int, elapsed: Int) { 41 | Logger.getLogger(TAG).log(Level.INFO, "onUserJoined: $uid") 42 | super.onUserJoined(uid, elapsed) 43 | this.hostView.remoteUserIDs.add(uid) 44 | 45 | this.hostView.rtcOverrideHandler?.onUserJoined(uid, elapsed) 46 | } 47 | 48 | override fun onRemoteAudioStateChanged(uid: Int, state: Int, reason: Int, elapsed: Int) { 49 | super.onRemoteAudioStateChanged(uid, state, reason, elapsed) 50 | Logger.getLogger(TAG).log(Level.WARNING, "setting muted state: $state") 51 | (this.hostView.context as Activity).runOnUiThread { 52 | if (state == Constants.REMOTE_AUDIO_STATE_STOPPED || state == Constants.REMOTE_AUDIO_STATE_STARTING) { 53 | if (state == Constants.REMOTE_AUDIO_STATE_STARTING && !this.hostView.userVideoLookup.containsKey( 54 | uid 55 | ) 56 | ) { 57 | this.hostView.addUserVideo(uid) 58 | } 59 | if (this.hostView.userVideoLookup.containsKey(uid)) { 60 | this.hostView.userVideoLookup[uid]?.audioMuted = 61 | state == Constants.REMOTE_AUDIO_STATE_STOPPED 62 | } 63 | } 64 | } 65 | 66 | this.hostView.rtcOverrideHandler?.onRemoteAudioStateChanged(uid, state, reason, elapsed) 67 | } 68 | 69 | override fun onUserOffline(uid: Int, reason: Int) { 70 | super.onUserOffline(uid, reason) 71 | Logger.getLogger(TAG).log(Level.WARNING, "User offline: $reason") 72 | if (reason == Constants.USER_OFFLINE_QUIT || reason == Constants.USER_OFFLINE_DROPPED) { 73 | this.hostView.remoteUserIDs.remove(uid) 74 | } 75 | if (this.hostView.userVideoLookup.containsKey(uid)) { 76 | (this.hostView.context as Activity).runOnUiThread { 77 | this.hostView.removeUserVideo(uid) 78 | } 79 | } 80 | 81 | this.hostView.rtcOverrideHandler?.onUserOffline(uid, reason) 82 | } 83 | 84 | override fun onActiveSpeaker(uid: Int) { 85 | super.onActiveSpeaker(uid) 86 | this.hostView.activeSpeaker = uid 87 | 88 | this.hostView.rtcOverrideHandler?.onActiveSpeaker(uid) 89 | } 90 | 91 | override fun onRemoteVideoStateChanged(uid: Int, state: Int, reason: Int, elapsed: Int) { 92 | super.onRemoteVideoStateChanged(uid, state, reason, elapsed) 93 | (this.hostView.context as Activity).runOnUiThread { 94 | when (state) { 95 | Constants.REMOTE_VIDEO_STATE_PLAYING -> { 96 | if (!this.hostView.userVideoLookup.containsKey(uid)) { 97 | this.hostView.addUserVideo(uid) 98 | } 99 | this.hostView.userVideoLookup[uid]?.videoMuted = false 100 | if (this.hostView.activeSpeaker == null && uid != this.hostView.userID) { 101 | this.hostView.activeSpeaker = uid 102 | } 103 | } 104 | Constants.REMOTE_VIDEO_STATE_STOPPED -> { 105 | this.hostView.userVideoLookup[uid]?.videoMuted = true 106 | } 107 | } 108 | } 109 | 110 | this.hostView.rtcOverrideHandler?.onRemoteVideoStateChanged(uid, state, reason, elapsed) 111 | } 112 | 113 | override fun onLocalVideoStateChanged( 114 | source: Constants.VideoSourceType?, 115 | state: Int, 116 | error: Int 117 | ) { 118 | super.onLocalVideoStateChanged(source, state, error) 119 | (this.hostView.context as Activity).runOnUiThread { 120 | if (state == Constants.LOCAL_VIDEO_STREAM_STATE_CAPTURING || state == Constants.LOCAL_VIDEO_STREAM_STATE_ENCODING || state == Constants.LOCAL_VIDEO_STREAM_STATE_STOPPED) { 121 | this.hostView.userVideoLookup[this.hostView.userID]?.videoMuted = state == Constants.LOCAL_VIDEO_STREAM_STATE_STOPPED 122 | } 123 | } 124 | this.hostView.rtcOverrideHandler?.onLocalVideoStateChanged(source, state, error) 125 | } 126 | 127 | override fun onLocalAudioStateChanged(state: Int, error: Int) { 128 | super.onLocalAudioStateChanged(state, error) 129 | (this.hostView.context as Activity).runOnUiThread { 130 | when (state) { 131 | Constants.LOCAL_AUDIO_STREAM_STATE_RECORDING, Constants.LOCAL_AUDIO_STREAM_STATE_STOPPED, Constants.LOCAL_AUDIO_STREAM_STATE_ENCODING -> { 132 | this.hostView.userVideoLookup[ 133 | this.hostView.userID 134 | ]?.audioMuted = state == Constants.LOCAL_AUDIO_STREAM_STATE_STOPPED 135 | } 136 | } 137 | } 138 | 139 | this.hostView.rtcOverrideHandler?.onLocalAudioStateChanged(state, error) 140 | } 141 | override fun onFirstLocalAudioFramePublished(elapsed: Int) { 142 | super.onFirstLocalAudioFramePublished(elapsed) 143 | (this.hostView.context as Activity).runOnUiThread { 144 | this.hostView.addLocalVideo()?.audioMuted = false 145 | } 146 | 147 | this.hostView.rtcOverrideHandler?.onFirstLocalAudioFramePublished(elapsed) 148 | } 149 | 150 | override fun onJoinChannelSuccess(channel: String, uid: Int, elapsed: Int) { 151 | super.onJoinChannelSuccess(channel, uid, elapsed) 152 | 153 | this.hostView.connectionData.channel = channel 154 | Logger.getLogger(TAG).log(Level.SEVERE, "join channel success") 155 | this.hostView.userID = uid 156 | if (this.hostView.userRole == Constants.CLIENT_ROLE_BROADCASTER) { 157 | (this.hostView.context as Activity).runOnUiThread { 158 | this.hostView.addLocalVideo() 159 | } 160 | } 161 | channel.let { 162 | this.hostView.delegate?.joinedChannel(it) 163 | } 164 | this.hostView.isInRtcChannel = true 165 | if (this.hostView.agoraRtmController.loginStatus != AgoraRtmController.LoginStatus.LOGGED_IN) { 166 | this.hostView.triggerLoginToRtm() 167 | } 168 | 169 | this.hostView.rtcOverrideHandler?.onJoinChannelSuccess(channel, uid, elapsed) 170 | } 171 | 172 | override fun onTokenPrivilegeWillExpire(token: String?) { 173 | super.onTokenPrivilegeWillExpire(token) 174 | if (this.hostView.delegate?.tokenWillExpire(token) == true) { 175 | return 176 | } 177 | this.hostView.fetchRenewToken() 178 | 179 | this.hostView.rtcOverrideHandler?.onTokenPrivilegeWillExpire(token) 180 | } 181 | 182 | override fun onRequestToken() { 183 | super.onRequestToken() 184 | if (this.hostView.delegate?.tokenDidExpire() == true) { 185 | return 186 | } 187 | this.hostView.fetchRenewToken() 188 | 189 | this.hostView.rtcOverrideHandler?.onRequestToken() 190 | } 191 | 192 | override fun onAudioEffectFinished(soundId: Int) { 193 | super.onAudioEffectFinished(soundId) 194 | 195 | this.hostView.rtcOverrideHandler?.onAudioEffectFinished(soundId) 196 | } 197 | 198 | override fun onAudioMixingStateChanged(state: Int, reason: Int) { 199 | super.onAudioMixingStateChanged(state, reason) 200 | 201 | this.hostView.rtcOverrideHandler?.onAudioMixingStateChanged(state, reason) 202 | } 203 | 204 | override fun onAudioPublishStateChanged( 205 | channel: String?, 206 | oldState: Int, 207 | newState: Int, 208 | elapseSinceLastState: Int 209 | ) { 210 | super.onAudioPublishStateChanged(channel, oldState, newState, elapseSinceLastState) 211 | 212 | this.hostView.rtcOverrideHandler?.onAudioPublishStateChanged(channel, oldState, newState, elapseSinceLastState) 213 | } 214 | 215 | override fun onAudioRouteChanged(routing: Int) { 216 | super.onAudioRouteChanged(routing) 217 | 218 | this.hostView.rtcOverrideHandler?.onAudioRouteChanged(routing) 219 | } 220 | 221 | override fun onAudioSubscribeStateChanged( 222 | channel: String?, 223 | uid: Int, 224 | oldState: Int, 225 | newState: Int, 226 | elapseSinceLastState: Int 227 | ) { 228 | super.onAudioSubscribeStateChanged(channel, uid, oldState, newState, elapseSinceLastState) 229 | 230 | this.hostView.rtcOverrideHandler?.onAudioSubscribeStateChanged(channel, uid, oldState, newState, elapseSinceLastState) 231 | } 232 | override fun onAudioVolumeIndication(speakers: Array?, totalVolume: Int) { 233 | super.onAudioVolumeIndication(speakers, totalVolume) 234 | 235 | this.hostView.rtcOverrideHandler?.onAudioVolumeIndication(speakers, totalVolume) 236 | } 237 | 238 | override fun onCameraExposureAreaChanged(rect: Rect?) { 239 | super.onCameraExposureAreaChanged(rect) 240 | 241 | this.hostView.rtcOverrideHandler?.onCameraExposureAreaChanged(rect) 242 | } 243 | 244 | override fun onCameraFocusAreaChanged(rect: Rect?) { 245 | super.onCameraFocusAreaChanged(rect) 246 | 247 | this.hostView.rtcOverrideHandler?.onCameraExposureAreaChanged(rect) 248 | } 249 | 250 | override fun onChannelMediaRelayEvent(code: Int) { 251 | super.onChannelMediaRelayEvent(code) 252 | 253 | this.hostView.rtcOverrideHandler?.onChannelMediaRelayEvent(code) 254 | } 255 | 256 | override fun onChannelMediaRelayStateChanged(state: Int, code: Int) { 257 | super.onChannelMediaRelayStateChanged(state, code) 258 | 259 | this.hostView.rtcOverrideHandler?.onChannelMediaRelayStateChanged(state, code) 260 | } 261 | 262 | override fun onConnectionLost() { 263 | super.onConnectionLost() 264 | 265 | this.hostView.rtcOverrideHandler?.onConnectionLost() 266 | } 267 | 268 | override fun onConnectionStateChanged(state: Int, reason: Int) { 269 | super.onConnectionStateChanged(state, reason) 270 | 271 | this.hostView.rtcOverrideHandler?.onConnectionStateChanged(state, reason) 272 | } 273 | 274 | override fun onContentInspectResult(result: Int) { 275 | super.onContentInspectResult(result) 276 | 277 | this.hostView.rtcOverrideHandler?.onContentInspectResult(result) 278 | } 279 | 280 | override fun onError(err: Int) { 281 | super.onError(err) 282 | 283 | this.hostView.rtcOverrideHandler?.onError(err) 284 | } 285 | 286 | override fun onFacePositionChanged( 287 | imageWidth: Int, 288 | imageHeight: Int, 289 | faces: Array? 290 | ) { 291 | super.onFacePositionChanged(imageWidth, imageHeight, faces) 292 | 293 | this.hostView.rtcOverrideHandler?.onFacePositionChanged(imageHeight, imageHeight, faces) 294 | } 295 | 296 | override fun onFirstLocalVideoFrame( 297 | source: Constants.VideoSourceType?, 298 | width: Int, 299 | height: Int, 300 | elapsed: Int 301 | ) { 302 | super.onFirstLocalVideoFrame(source, width, height, elapsed) 303 | 304 | this.hostView.rtcOverrideHandler?.onFirstLocalVideoFrame(source, width, height, elapsed) 305 | } 306 | 307 | override fun onFirstLocalVideoFramePublished(source: Constants.VideoSourceType?, elapsed: Int) { 308 | super.onFirstLocalVideoFramePublished(source, elapsed) 309 | 310 | this.hostView.rtcOverrideHandler?.onFirstLocalVideoFramePublished(source, elapsed) 311 | } 312 | 313 | override fun onFirstRemoteVideoFrame(uid: Int, width: Int, height: Int, elapsed: Int) { 314 | super.onFirstRemoteVideoFrame(uid, width, height, elapsed) 315 | 316 | this.hostView.rtcOverrideHandler?.onFirstRemoteVideoFrame(uid, width, height, elapsed) 317 | } 318 | 319 | override fun onLastmileProbeResult(result: LastmileProbeResult?) { 320 | super.onLastmileProbeResult(result) 321 | 322 | this.hostView.rtcOverrideHandler?.onLastmileProbeResult(result) 323 | } 324 | 325 | override fun onLastmileQuality(quality: Int) { 326 | super.onLastmileQuality(quality) 327 | 328 | this.hostView.rtcOverrideHandler?.onLastmileQuality(quality) 329 | } 330 | 331 | override fun onLeaveChannel(stats: RtcStats?) { 332 | super.onLeaveChannel(stats) 333 | 334 | this.hostView.rtcOverrideHandler?.onLeaveChannel(stats) 335 | } 336 | 337 | override fun onLocalAudioStats(stats: LocalAudioStats?) { 338 | super.onLocalAudioStats(stats) 339 | 340 | this.hostView.rtcOverrideHandler?.onLocalAudioStats(stats) 341 | } 342 | 343 | override fun onLocalPublishFallbackToAudioOnly(isFallbackOrRecover: Boolean) { 344 | super.onLocalPublishFallbackToAudioOnly(isFallbackOrRecover) 345 | 346 | this.hostView.rtcOverrideHandler?.onLocalPublishFallbackToAudioOnly(isFallbackOrRecover) 347 | } 348 | 349 | override fun onLocalUserRegistered(uid: Int, userAccount: String?) { 350 | super.onLocalUserRegistered(uid, userAccount) 351 | 352 | this.hostView.rtcOverrideHandler?.onLocalUserRegistered(uid, userAccount) 353 | } 354 | 355 | override fun onLocalVideoStats(source: Constants.VideoSourceType?, stats: LocalVideoStats?) { 356 | super.onLocalVideoStats(source, stats) 357 | 358 | this.hostView.rtcOverrideHandler?.onLocalVideoStats(source, stats) 359 | } 360 | 361 | override fun onMediaEngineLoadSuccess() { 362 | super.onMediaEngineLoadSuccess() 363 | 364 | this.hostView.rtcOverrideHandler?.onMediaEngineLoadSuccess() 365 | } 366 | 367 | override fun onMediaEngineStartCallSuccess() { 368 | super.onMediaEngineStartCallSuccess() 369 | 370 | this.hostView.rtcOverrideHandler?.onMediaEngineStartCallSuccess() 371 | } 372 | 373 | override fun onNetworkQuality(uid: Int, txQuality: Int, rxQuality: Int) { 374 | super.onNetworkQuality(uid, txQuality, rxQuality) 375 | 376 | this.hostView.rtcOverrideHandler?.onNetworkQuality(uid, txQuality, rxQuality) 377 | } 378 | 379 | override fun onNetworkTypeChanged(type: Int) { 380 | super.onNetworkTypeChanged(type) 381 | 382 | this.hostView.rtcOverrideHandler?.onNetworkTypeChanged(type) 383 | } 384 | 385 | override fun onRejoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) { 386 | super.onRejoinChannelSuccess(channel, uid, elapsed) 387 | 388 | this.hostView.rtcOverrideHandler?.onRejoinChannelSuccess(channel, uid, elapsed) 389 | } 390 | 391 | override fun onRemoteAudioStats(stats: RemoteAudioStats?) { 392 | super.onRemoteAudioStats(stats) 393 | 394 | this.hostView.rtcOverrideHandler?.onRemoteAudioStats(stats) 395 | } 396 | 397 | override fun onRemoteSubscribeFallbackToAudioOnly(uid: Int, isFallbackOrRecover: Boolean) { 398 | super.onRemoteSubscribeFallbackToAudioOnly(uid, isFallbackOrRecover) 399 | 400 | this.hostView.rtcOverrideHandler?.onRemoteSubscribeFallbackToAudioOnly(uid, isFallbackOrRecover) 401 | } 402 | 403 | override fun onRemoteVideoStats(stats: RemoteVideoStats?) { 404 | super.onRemoteVideoStats(stats) 405 | 406 | this.hostView.rtcOverrideHandler?.onRemoteVideoStats(stats) 407 | } 408 | 409 | override fun onRtcStats(stats: RtcStats?) { 410 | super.onRtcStats(stats) 411 | 412 | this.hostView.rtcOverrideHandler?.onRtcStats(stats) 413 | } 414 | 415 | override fun onRtmpStreamingStateChanged(url: String?, state: Int, errCode: Int) { 416 | super.onRtmpStreamingStateChanged(url, state, errCode) 417 | 418 | this.hostView.rtcOverrideHandler?.onRtmpStreamingStateChanged(url, state, errCode) 419 | } 420 | 421 | override fun onSnapshotTaken( 422 | uid: Int, 423 | filePath: String?, 424 | width: Int, 425 | height: Int, 426 | errCode: Int 427 | ) { 428 | super.onSnapshotTaken(uid, filePath, width, height, errCode) 429 | 430 | this.hostView.rtcOverrideHandler?.onSnapshotTaken(uid, filePath, width, height, errCode) 431 | } 432 | 433 | override fun onStreamInjectedStatus(url: String?, uid: Int, status: Int) { 434 | super.onStreamInjectedStatus(url, uid, status) 435 | 436 | this.hostView.rtcOverrideHandler?.onStreamInjectedStatus(url, uid, status) 437 | } 438 | 439 | override fun onStreamMessage(uid: Int, streamId: Int, data: ByteArray?) { 440 | super.onStreamMessage(uid, streamId, data) 441 | 442 | this.hostView.rtcOverrideHandler?.onStreamMessage(uid, streamId, data) 443 | } 444 | 445 | override fun onStreamMessageError( 446 | uid: Int, 447 | streamId: Int, 448 | error: Int, 449 | missed: Int, 450 | cached: Int 451 | ) { 452 | super.onStreamMessageError(uid, streamId, error, missed, cached) 453 | 454 | this.hostView.rtcOverrideHandler?.onStreamMessageError(uid, streamId, error, missed, cached) 455 | } 456 | 457 | override fun onTranscodingUpdated() { 458 | super.onTranscodingUpdated() 459 | 460 | this.hostView.rtcOverrideHandler?.onTranscodingUpdated() 461 | } 462 | 463 | override fun onUploadLogResult(requestId: String?, success: Boolean, reason: Int) { 464 | super.onUploadLogResult(requestId, success, reason) 465 | 466 | this.hostView.rtcOverrideHandler?.onUploadLogResult(requestId, success, reason) 467 | } 468 | 469 | override fun onUserInfoUpdated(uid: Int, userInfo: UserInfo?) { 470 | super.onUserInfoUpdated(uid, userInfo) 471 | 472 | this.hostView.rtcOverrideHandler?.onUserInfoUpdated(uid, userInfo) 473 | } 474 | 475 | override fun onUserMuteAudio(uid: Int, muted: Boolean) { 476 | super.onUserMuteAudio(uid, muted) 477 | 478 | this.hostView.rtcOverrideHandler?.onUserMuteAudio(uid, muted) 479 | } 480 | 481 | override fun onUserMuteVideo(uid: Int, muted: Boolean) { 482 | super.onUserMuteVideo(uid, muted) 483 | 484 | this.hostView.rtcOverrideHandler?.onUserMuteVideo(uid, muted) 485 | } 486 | 487 | override fun onVideoPublishStateChanged( 488 | source: Constants.VideoSourceType?, 489 | channel: String?, 490 | oldState: Int, 491 | newState: Int, 492 | elapseSinceLastState: Int 493 | ) { 494 | super.onVideoPublishStateChanged(source, channel, oldState, newState, elapseSinceLastState) 495 | 496 | this.hostView.rtcOverrideHandler?.onVideoPublishStateChanged(source, channel, oldState, newState, elapseSinceLastState) 497 | } 498 | 499 | override fun onVideoSizeChanged( 500 | source: Constants.VideoSourceType?, 501 | uid: Int, 502 | width: Int, 503 | height: Int, 504 | rotation: Int 505 | ) { 506 | super.onVideoSizeChanged(source, uid, width, height, rotation) 507 | 508 | this.hostView.rtcOverrideHandler?.onVideoSizeChanged(source, uid, width, height, rotation) 509 | } 510 | 511 | override fun onVideoSubscribeStateChanged( 512 | channel: String?, 513 | uid: Int, 514 | oldState: Int, 515 | newState: Int, 516 | elapseSinceLastState: Int 517 | ) { 518 | super.onVideoSubscribeStateChanged(channel, uid, oldState, newState, elapseSinceLastState) 519 | 520 | this.hostView.rtcOverrideHandler?.onVideoSubscribeStateChanged(channel, uid, oldState, newState, elapseSinceLastState) 521 | } 522 | 523 | override fun onAudioMixingFinished() { 524 | super.onAudioMixingFinished() 525 | 526 | this.hostView.rtcOverrideHandler?.onAudioMixingFinished() 527 | } 528 | 529 | override fun onConnectionBanned() { 530 | super.onConnectionBanned() 531 | 532 | this.hostView.rtcOverrideHandler?.onConnectionBanned() 533 | } 534 | 535 | override fun onConnectionInterrupted() { 536 | super.onConnectionInterrupted() 537 | 538 | this.hostView.rtcOverrideHandler?.onConnectionInterrupted() 539 | } 540 | 541 | override fun onIntraRequestReceived() { 542 | super.onIntraRequestReceived() 543 | 544 | this.hostView.rtcOverrideHandler?.onIntraRequestReceived() 545 | } 546 | 547 | override fun onDownlinkNetworkInfoUpdated(info: DownlinkNetworkInfo?) { 548 | super.onDownlinkNetworkInfoUpdated(info) 549 | 550 | this.hostView.rtcOverrideHandler?.onDownlinkNetworkInfoUpdated(info) 551 | } 552 | 553 | override fun onCameraReady() { 554 | super.onCameraReady() 555 | 556 | this.hostView.rtcOverrideHandler?.onCameraReady() 557 | } 558 | 559 | override fun onEncryptionError(errorType: Int) { 560 | super.onEncryptionError(errorType) 561 | 562 | this.hostView.rtcOverrideHandler?.onEncryptionError(errorType) 563 | } 564 | 565 | override fun onVideoStopped() { 566 | super.onVideoStopped() 567 | 568 | this.hostView.rtcOverrideHandler?.onVideoStopped() 569 | } 570 | 571 | override fun onPermissionError(permission: Int) { 572 | super.onPermissionError(permission) 573 | 574 | this.hostView.rtcOverrideHandler?.onPermissionError(permission) 575 | } 576 | 577 | override fun onAudioQuality(uid: Int, quality: Int, delay: Short, lost: Short) { 578 | super.onAudioQuality(uid, quality, delay, lost) 579 | 580 | this.hostView.rtcOverrideHandler?.onAudioQuality(uid, quality, delay, lost) 581 | } 582 | 583 | override fun onUplinkNetworkInfoUpdated(info: UplinkNetworkInfo?) { 584 | super.onUplinkNetworkInfoUpdated(info) 585 | 586 | this.hostView.rtcOverrideHandler?.onUplinkNetworkInfoUpdated(info) 587 | } 588 | 589 | override fun onFirstRemoteAudioDecoded(uid: Int, elapsed: Int) { 590 | super.onFirstRemoteAudioDecoded(uid, elapsed) 591 | 592 | this.hostView.rtcOverrideHandler?.onFirstRemoteAudioDecoded(uid, elapsed) 593 | } 594 | 595 | override fun onFirstRemoteAudioFrame(uid: Int, elapsed: Int) { 596 | super.onFirstRemoteAudioFrame(uid, elapsed) 597 | 598 | this.hostView.rtcOverrideHandler?.onFirstRemoteAudioFrame(uid, elapsed) 599 | } 600 | 601 | override fun onRemoteAudioTransportStats(uid: Int, delay: Int, lost: Int, rxKBitRate: Int) { 602 | super.onRemoteAudioTransportStats(uid, delay, lost, rxKBitRate) 603 | 604 | this.hostView.rtcOverrideHandler?.onRemoteAudioTransportStats(uid, delay, lost, rxKBitRate) 605 | } 606 | 607 | override fun onRemoteVideoTransportStats(uid: Int, delay: Int, lost: Int, rxKBitRate: Int) { 608 | super.onRemoteVideoTransportStats(uid, delay, lost, rxKBitRate) 609 | 610 | this.hostView.rtcOverrideHandler?.onRemoteVideoTransportStats(uid, delay, lost, rxKBitRate) 611 | } 612 | 613 | override fun onRhythmPlayerStateChanged(state: Int, errorCode: Int) { 614 | super.onRhythmPlayerStateChanged(state, errorCode) 615 | 616 | this.hostView.rtcOverrideHandler?.onRhythmPlayerStateChanged(state, errorCode) 617 | } 618 | 619 | override fun onClientRoleChangeFailed(reason: Int, currentRole: Int) { 620 | super.onClientRoleChangeFailed(reason, currentRole) 621 | 622 | this.hostView.rtcOverrideHandler?.onClientRoleChangeFailed(reason, currentRole) 623 | } 624 | 625 | override fun onRtmpStreamingEvent(url: String?, event: Int) { 626 | super.onRtmpStreamingEvent(url, event) 627 | 628 | this.hostView.rtcOverrideHandler?.onRtmpStreamingEvent(url, event) 629 | } 630 | 631 | override fun onProxyConnected( 632 | channel: String?, 633 | uid: Int, 634 | proxyType: Int, 635 | localProxyIp: String?, 636 | elapsed: Int 637 | ) { 638 | super.onProxyConnected(channel, uid, proxyType, localProxyIp, elapsed) 639 | 640 | this.hostView.rtcOverrideHandler?.onProxyConnected(channel, uid, proxyType, localProxyIp, elapsed) 641 | } 642 | 643 | override fun onUserStateChanged(uid: Int, state: Int) { 644 | super.onUserStateChanged(uid, state) 645 | 646 | this.hostView.rtcOverrideHandler?.onUserStateChanged(uid, state) 647 | } 648 | 649 | override fun onWlAccMessage(reason: Int, action: Int, wlAccMsg: String?) { 650 | super.onWlAccMessage(reason, action, wlAccMsg) 651 | 652 | this.hostView.rtcOverrideHandler?.onWlAccMessage(reason, action, wlAccMsg) 653 | } 654 | 655 | override fun onWlAccStats(currentStats: WlAccStats?, averageStats: WlAccStats?) { 656 | super.onWlAccStats(currentStats, averageStats) 657 | 658 | this.hostView.rtcOverrideHandler?.onWlAccStats(currentStats, averageStats) 659 | } 660 | } 661 | --------------------------------------------------------------------------------