("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 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
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 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | xmlns:android
18 |
19 | ^$
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | xmlns:.*
29 |
30 | ^$
31 |
32 |
33 | BY_NAME
34 |
35 |
36 |
37 |
38 |
39 |
40 | .*:id
41 |
42 | http://schemas.android.com/apk/res/android
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 | .*:name
52 |
53 | http://schemas.android.com/apk/res/android
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 | name
63 |
64 | ^$
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 | style
74 |
75 | ^$
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 | .*
85 |
86 | ^$
87 |
88 |
89 | BY_NAME
90 |
91 |
92 |
93 |
94 |
95 |
96 | .*
97 |
98 | http://schemas.android.com/apk/res/android
99 |
100 |
101 | ANDROID_ATTRIBUTE_ORDER
102 |
103 |
104 |
105 |
106 |
107 |
108 | .*
109 |
110 | .*
111 |
112 |
113 | BY_NAME
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
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 |
--------------------------------------------------------------------------------