I am using Agora for the video call function. I have run the sample code of agora for 1-1 video call referencing this - https://docs.agora.io/en/Video/start_call_android?platform=Android
In the sample we have one local view and one remote view, this is working well. Now I want to show two remote views and one local view. For that I created layout which has two remote views and one local view.
Also in onFirstRemoteVideoDecoded method I checked if the 1st remote view is added then setup 1st remote view else setup 2nd remoteview. Same for onUserOffline when the user is left I checked if 1st remote view is logged of the run method onRemoteUserLeft() or onRemoteUserLeft1() which removes the related views.
To join the channel I am providing same channel name and different tokens
`
var token1: String? = ("<token-string>")
if (token1!!.isEmpty()) {
token1 = null
}
mRtcEngine!!.joinChannel(
token1,
"67124678",
"null",
0
)
I have a link which provide the channel name and the related token. https://mobile.quirkysemicolon.com/getTokens.php
I am confused about how can I use this link in my app to join multiple user on same channel.
Can anyone suggest how can I achieve this functionality?
Here is my layout:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<FrameLayout
android:id="@+id/remote_video_view_container"
android:layout_width="0dp"
android:layout_height="284dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:background="@android:color/darker_gray"
app:layout_constraintEnd_toStartOf="@+id/remote1_video_view_container"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/remote1_video_view_container"
android:layout_width="0dp"
android:layout_height="284dp"
android:layout_alignParentTop="true"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:background="@android:color/darker_gray"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/remote_video_view_container"
app:layout_constraintTop_toTopOf="parent" />
<FrameLayout
android:id="@+id/local_video_view_container"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/white"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/container"
app:layout_constraintTop_toBottomOf="@+id/remote1_video_view_container" />
<RelativeLayout
android:id="@+id/container"
android:layout_width="0dp"
android:layout_height="0dp"
android:background="@android:color/white"
android:padding="20dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintEnd_toStartOf="@+id/local_video_view_container"
app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/remote1_video_view_container">
<TextView
android:id="@+id/textView2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_gravity="center_horizontal"
android:layout_marginTop="20dp"
android:textSize="18sp"
android:textColor="@android:color/black"
android:text="Channel :" />
<Button
android:id="@+id/button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start"
android:textColor="@android:color/white"
android:background="@android:color/holo_green_dark"
android:layout_marginTop="20dp"
android:layout_gravity="center_horizontal"
android:layout_below="@id/textView2"/>
<Button
android:id="@+id/button2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="End"
android:onClick="onEndCallClicked"
android:textColor="@android:color/white"
android:background="@android:color/holo_red_dark"
android:layout_marginTop="20dp"
android:layout_gravity="center_horizontal"
android:layout_below="@id/button"/>
<TextView
android:id="@+id/textView5"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/button2"
android:layout_marginTop="20dp"
android:textSize="18sp"
android:textColor="@android:color/black"
android:layout_gravity="center_horizontal"
android:text="Token(Push Notify) : " />
</RelativeLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
Ans here is my activity
class MultipleRemoteViewActivity: AppCompatActivity() {
private var mRtcEngine: RtcEngine? = null
private var mReceiver: BroadcastReceiver? = null
var mIntentFilter: IntentFilter? = null
var mIsRemote1Joined: Boolean = false
var mIsRemote1Left: Boolean = false
private val mRtcEventHandler = object : IRtcEngineEventHandler() {
/**
* Occurs when the first remote video frame is received and decoded.
* This callback is triggered in either of the following scenarios:
*
* The remote user joins the channel and sends the video stream.
* The remote user stops sending the video stream and re-sends it after 15 seconds. Possible reasons include:
* The remote user leaves channel.
* The remote user drops offline.
* The remote user calls the muteLocalVideoStream method.
* The remote user calls the disableVideo method.
*
* @param uid User ID of the remote user sending the video streams.
* @param width Width (pixels) of the video stream.
* @param height Height (pixels) of the video stream.
* @param elapsed Time elapsed (ms) from the local user calling the joinChannel method until this callback is triggered.
*/
override fun onFirstRemoteVideoDecoded(uid: Int, width: Int, height: Int, elapsed: Int) {
runOnUiThread {
if(!mIsRemote1Joined)
setupRemoteVideo(uid)
else
setupRemoteVideo1(uid)
}
}
override fun onUserOffline(uid: Int, reason: Int) {
runOnUiThread {
if(!mIsRemote1Left)
onRemoteUserLeft()
else
onRemoteUserLeft1()
}
}
override fun onUserMuteVideo(uid: Int, muted: Boolean) {
runOnUiThread { onRemoteUserVideoMuted(uid, muted) }
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.multiple_remoteview_layout)
if (checkSelfPermission(Manifest.permission.RECORD_AUDIO,
MultipleRemoteViewActivity.PERMISSION_REQ_ID_RECORD_AUDIO
) && checkSelfPermission(
Manifest.permission.CAMERA, MultipleRemoteViewActivity.PERMISSION_REQ_ID_CAMERA
)) {
initAgoraEngineAndJoinChannel()
}
}
private fun initAgoraEngineAndJoinChannel() {
initializeAgoraEngine()
}
private fun checkSelfPermission(permission: String, requestCode: Int): Boolean {
Log.i(MultipleRemoteViewActivity.LOG_TAG, "checkSelfPermission $permission $requestCode")
if (ContextCompat.checkSelfPermission(
this,
permission
) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(
this,
arrayOf(permission),
requestCode
)
return false
}
return true
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<String>, grantResults: IntArray
) {
Log.i(MultipleRemoteViewActivity.LOG_TAG, "onRequestPermissionsResult " + grantResults[0] + " " + requestCode)
when (requestCode) {
MultipleRemoteViewActivity.PERMISSION_REQ_ID_RECORD_AUDIO -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
checkSelfPermission(Manifest.permission.CAMERA,
MultipleRemoteViewActivity.PERMISSION_REQ_ID_CAMERA
)
} else {
showLongToast("No permission for " + Manifest.permission.RECORD_AUDIO)
finish()
}
}
MultipleRemoteViewActivity.PERMISSION_REQ_ID_CAMERA -> {
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initAgoraEngineAndJoinChannel()
} else {
showLongToast("No permission for " + Manifest.permission.CAMERA)
finish()
}
}
}
}
private fun showLongToast(msg: String) {
this.runOnUiThread { Toast.makeText(applicationContext, msg, Toast.LENGTH_LONG).show() }
}
override fun onDestroy() {
super.onDestroy()
leaveChannel()
/*
Destroys the RtcEngine instance and releases all resources used by the Agora SDK.
This method is useful for apps that occasionally make voice or video calls,
to free up resources for other operations when not making calls.
*/
RtcEngine.destroy()
mRtcEngine = null
}
fun onLocalVideoMuteClicked(view: View) {
/* val iv = view as ImageView
if (iv.isSelected) {
iv.isSelected = false
iv.clearColorFilter()
} else {
iv.isSelected = true
iv.setColorFilter(resources.getColor(R.color.colorPrimary), PorterDuff.Mode.MULTIPLY)
}
// Stops/Resumes sending the local video stream.
mRtcEngine!!.muteLocalVideoStream(iv.isSelected)*/
val container = findViewById(R.id.local_video_view_container) as FrameLayout
val surfaceView = container.getChildAt(0) as SurfaceView
// surfaceView.setZOrderMediaOverlay(!iv.isSelected) // surfaceView.visibility = if (iv.isSelected) View.GONE else View.VISIBLE
}
/* fun onLocalAudioMuteClicked(view: View) {
val iv = view as ImageView
if (iv.isSelected) {
iv.isSelected = false
iv.clearColorFilter()
} else {
iv.isSelected = true
iv.setColorFilter(resources.getColor(R.color.colorPrimary), PorterDuff.Mode.MULTIPLY)
}
// Stops/Resumes sending the local audio stream.
mRtcEngine!!.muteLocalAudioStream(iv.isSelected)
}*/
/*
fun onSwitchCameraClicked(view: View) {
// Switches between front and rear cameras.
mRtcEngine!!.switchCamera()
}
*/
fun onEndCallClicked(view: View) {
finish()
}
private fun initializeAgoraEngine() {
try {
mRtcEngine = RtcEngine.create(
baseContext,
getString(R.string.agora_app_id),
mRtcEventHandler
)
setupVideoProfile()
setupLocalVideo()
joinChannel()
} catch (e: Exception) {
Log.e(MultipleRemoteViewActivity.LOG_TAG, Log.getStackTraceString(e))
throw RuntimeException(
"NEED TO check rtc sdk init fatal error\n" + Log.getStackTraceString(e)
)
}
}
private fun setupVideoProfile() {
// In simple use cases, we only need to enable video capturing
// and rendering once at the initialization step.
// Note: audio recording and playing is enabled by default.
mRtcEngine!!.enableVideo()
// mRtcEngine!!.setVideoProfile(Constants.VIDEO_PROFILE_360P, false) // Earlier than 2.3.0
// Please go to this page for detailed explanation
// https://docs.agora.io/en/Video/API%20Reference/java/classio_1_1agora_1_1rtc_1_1_rtc_engine.html#af5f4de754e2c1f493096641c5c5c1d8f
mRtcEngine!!.setVideoEncoderConfiguration(
VideoEncoderConfiguration(
VideoEncoderConfiguration.VD_640x360,
VideoEncoderConfiguration.FRAME_RATE.FRAME_RATE_FPS_15,
VideoEncoderConfiguration.STANDARD_BITRATE,
VideoEncoderConfiguration.ORIENTATION_MODE.ORIENTATION_MODE_FIXED_PORTRAIT
)
)
}
private fun setupLocalVideo() {
val container = findViewById(R.id.local_video_view_container) as FrameLayout
val surfaceView = RtcEngine.CreateRendererView(baseContext)
surfaceView.setZOrderMediaOverlay(true)
container.addView(surfaceView)
// Initializes the local video view.
// RENDER_MODE_FIT: Uniformly scale the video until one of its dimension fits the boundary. Areas that are not filled due to the disparity in the aspect ratio are filled with black.
mRtcEngine!!.setupLocalVideo(VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, 0))
}
private fun joinChannel() {
// 1. Users can only see each other after they join the
// same channel successfully using the same app id.
// 2. One token is only valid for the channel name that
// you use to generate this token.
var token: String? = getString(R.string.agora_access_token)
if (token!!.isEmpty()) {
token = null
}
mRtcEngine!!.joinChannel(
token,
"67124678",
"null",
0
) // if you do not specify the uid, we will generate the uid for you
var token1: String? = ("<token-string>")
if (token1!!.isEmpty()) {
token1 = null
}
mRtcEngine!!.joinChannel(
token1,
"67124678",
"null",
0
) // if you do not specify the uid, we will generate the uid for you
}
private fun setupRemoteVideo(uid: Int) {
// Only one remote video view is available for this
// tutorial. Here we check if there exists a surface
// view tagged as this uid.
val container = findViewById(R.id.remote_video_view_container) as FrameLayout
if (container.childCount >= 1) {
return
}
/*
Creates the video renderer view.
CreateRendererView returns the SurfaceView type. The operation and layout of the view
are managed by the app, and the Agora SDK renders the view provided by the app.
The video display view must be created using this method instead of directly
calling SurfaceView.
*/
val surfaceView = RtcEngine.CreateRendererView(baseContext)
container.addView(surfaceView)
// Initializes the video view of a remote user.
mRtcEngine!!.setupRemoteVideo(VideoCanvas(surfaceView, VideoCanvas.RENDER_MODE_FIT, uid))
surfaceView.tag = uid // for mark purpose
mIsRemote1Joined = true
// val tipMsg = findViewById<TextView>(R.id.quick_tips_when_use_agora_sdk) // optional UI
// tipMsg.visibility = View.GONE
}
private fun setupRemoteVideo1(uid: Int) {
// Only one remote video view is available for this
// tutorial. Here we check if there exists a surface
// view tagged as this uid.
val container1 = findViewById(R.id.remote1_video_view_container) as FrameLayout
if (container1.childCount >= 1) {
return
}
/*
Creates the video renderer view.
CreateRendererView returns the SurfaceView type. The operation and layout of the view
are managed by the app, and the Agora SDK renders the view provided by the app.
The video display view must be created using this method instead of directly
calling SurfaceView.
*/
val surfaceView1 = RtcEngine.CreateRendererView(baseContext)
container1.addView(surfaceView1)
// Initializes the video view of a remote user.
mRtcEngine!!.setupRemoteVideo(VideoCanvas(surfaceView1, VideoCanvas.RENDER_MODE_FIT, uid))
surfaceView1.tag = uid // for mark purpose
// val tipMsg = findViewById<TextView>(R.id.quick_tips_when_use_agora_sdk) // optional UI
// tipMsg.visibility = View.GONE
}
private fun leaveChannel() {
mRtcEngine!!.leaveChannel()
}
private fun onRemoteUserLeft() {
val container = findViewById(R.id.remote_video_view_container) as FrameLayout
container.removeAllViews()
mIsRemote1Left = true
}
private fun onRemoteUserLeft1() {
val container1 = findViewById(R.id.remote1_video_view_container) as FrameLayout
container1.removeAllViews()
}
private fun onRemoteUserVideoMuted(uid: Int, muted: Boolean) {
val container = findViewById(R.id.remote_video_view_container) as FrameLayout
val surfaceView = container.getChildAt(0) as? SurfaceView
val tag = surfaceView?.tag
if (tag != null && tag as Int == uid) {
surfaceView.visibility = if (muted) View.GONE else View.VISIBLE
}
val container1 = findViewById(R.id.remote1_video_view_container) as FrameLayout
val surfaceView1 = container1.getChildAt(0) as? SurfaceView
val tag1 = surfaceView1?.tag
if (tag1 != null && tag1 as Int == uid) {
surfaceView1.visibility = if (muted) View.GONE else View.VISIBLE
}
}
companion object {
private val LOG_TAG = MultipleRemoteViewActivity::class.java.simpleName
private const val PERMISSION_REQ_ID_RECORD_AUDIO = 22
private const val PERMISSION_REQ_ID_CAMERA = PERMISSION_REQ_ID_RECORD_AUDIO + 1
}
}
Now issue inmy code is: I am joining same channel with same app id on three devices one is web- chrome app and other two on android devices. When I join from two devices I am able to see one remote view and one local view but when the three devices have joined the same channel and if one of the device leaves the channel both the remote views stop working and the unable to join the same again..
Please suggest the solution...