diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml
index 1a4c7e93..cc9f8fa0 100644
--- a/.github/workflows/gradle.yml
+++ b/.github/workflows/gradle.yml
@@ -190,7 +190,7 @@ jobs:
- name: Archive Test Report
if: always()
- uses: actions/upload-artifact@v3
+ uses: actions/upload-artifact@v4
with:
name: Test report
path: |
diff --git a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java
index c625c1e7..872f0241 100644
--- a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java
+++ b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/core/WebRTCClient.java
@@ -16,6 +16,7 @@
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.WindowManager;
+import android.widget.GridLayout;
import android.widget.Toast;
import androidx.annotation.NonNull;
@@ -414,6 +415,10 @@ public WebRTCClientConfig getConfig() {
return config;
}
+ public void setConfig(WebRTCClientConfig config) {
+ this.config = config;
+ }
+
public SDPObserver getSdpObserver(String streamId) {
return new SDPObserver(streamId);
}
@@ -764,6 +769,7 @@ public void initializeRenderers() {
config.localVideoRenderer.setEnableHardwareScaler(true /* enabled */);
localVideoSink.setTarget(config.localVideoRenderer);
}
+
}
public void initializePeerConnectionFactory() {
@@ -1203,10 +1209,8 @@ public void releaseRenderer(SurfaceViewRenderer renderer, VideoTrack track, Vide
renderer.clearAnimation();
mainHandler.postAtFrontOfQueue(renderer::clearImage);
- mainHandler.post(() -> {
- renderer.release();
- renderer.setTag(null);
- });
+ renderer.release();
+ renderer.setTag(null);
});
}
diff --git a/webrtc-android-sample-app/src/androidTest/java/io/antmedia/webrtc_android_sample_app/ConferenceActivityTest.java b/webrtc-android-sample-app/src/androidTest/java/io/antmedia/webrtc_android_sample_app/ConferenceActivityTest.java
index ad5fce2e..6b058d8d 100644
--- a/webrtc-android-sample-app/src/androidTest/java/io/antmedia/webrtc_android_sample_app/ConferenceActivityTest.java
+++ b/webrtc-android-sample-app/src/androidTest/java/io/antmedia/webrtc_android_sample_app/ConferenceActivityTest.java
@@ -435,7 +435,7 @@ public void testConferenceReconnect() throws IOException, InterruptedException {
onView(withId(R.id.multitrack_stats_popup_close_button)).perform(click());
- //Thread.sleep(5000);
+ Thread.sleep(5000);
onView(withId(R.id.join_conference_button)).perform(click());
@@ -465,7 +465,7 @@ public void testConferenceReconnect() throws IOException, InterruptedException {
onView(withId(R.id.broadcasting_text_view))
.check(matches(withText(R.string.live)));
- //Thread.sleep(3000);
+ Thread.sleep(3000);
onView(withId(R.id.join_conference_button)).perform(click());
diff --git a/webrtc-android-sample-app/src/androidTest/java/io/antmedia/webrtc_android_sample_app/DynamicConferenceActivityTest.java b/webrtc-android-sample-app/src/androidTest/java/io/antmedia/webrtc_android_sample_app/DynamicConferenceActivityTest.java
new file mode 100644
index 00000000..acf8f8f9
--- /dev/null
+++ b/webrtc-android-sample-app/src/androidTest/java/io/antmedia/webrtc_android_sample_app/DynamicConferenceActivityTest.java
@@ -0,0 +1,170 @@
+package io.antmedia.webrtc_android_sample_app;
+
+import static android.provider.Settings.System.getString;
+import static androidx.test.espresso.Espresso.onView;
+import static androidx.test.espresso.action.ViewActions.click;
+import static androidx.test.espresso.action.ViewActions.swipeUp;
+import static androidx.test.espresso.assertion.ViewAssertions.matches;
+import static androidx.test.espresso.matcher.RootMatchers.isDialog;
+import static androidx.test.espresso.matcher.ViewMatchers.isDisplayed;
+import static androidx.test.espresso.matcher.ViewMatchers.withId;
+import static androidx.test.espresso.matcher.ViewMatchers.withText;
+import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
+import static org.hamcrest.CoreMatchers.anyOf;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.util.Log;
+import android.widget.GridLayout;
+import android.widget.TextView;
+
+import androidx.preference.PreferenceManager;
+import androidx.recyclerview.widget.RecyclerView;
+import androidx.test.core.app.ActivityScenario;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.espresso.IdlingRegistry;
+import androidx.test.espresso.IdlingResource;
+
+import androidx.test.ext.junit.rules.ActivityScenarioRule;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.rule.GrantPermissionRule;
+import androidx.test.uiautomator.UiDevice;
+
+
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.junit.runner.RunWith;
+import org.webrtc.SurfaceViewRenderer;
+import org.webrtc.VideoSink;
+import org.webrtc.VideoTrack;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+
+import io.antmedia.webrtc_android_sample_app.basic.DynamicConferenceActivity;
+import io.antmedia.webrtc_android_sample_app.basic.SettingsActivity;
+import io.antmedia.webrtcandroidframework.core.PermissionHandler;
+import io.antmedia.webrtcandroidframework.core.WebRTCClient;
+
+
+/**
+ * Instrumented test, which will execute on an Android device.
+ *
+ * @see Testing documentation
+ */
+@RunWith(AndroidJUnit4.class)
+public class DynamicConferenceActivityTest {
+
+ @Rule
+ public GrantPermissionRule permissionRule
+ = GrantPermissionRule.grant(PermissionHandler.FULL_PERMISSIONS);
+
+ private IdlingResource mIdlingResource;
+ @Rule
+ public ActivityScenarioRule dynamicConferenceActivityScenarioRule = new ActivityScenarioRule<>(DynamicConferenceActivity.class);
+
+
+ private String runningTest;
+ private String roomName;
+
+ @Before
+ public void before() throws IOException {
+ System.out.println("before test");
+ connectInternet();
+
+ getInstrumentation().waitForIdleSync();
+ Context context = ApplicationProvider.getApplicationContext();
+ SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+ roomName = sharedPreferences.getString(context.getString(R.string.roomId), SettingsActivity.DEFAULT_ROOM_NAME);
+ }
+
+ @After
+ public void after() {
+ System.out.println("after test");
+ try {
+ Thread.sleep(1000);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+
+ }
+
+ @Rule
+ public TestWatcher watchman= new TestWatcher() {
+
+ @Override
+ protected void failed(Throwable e, Description description) {
+ Log.i("TestWatcher", "*** "+description + " failed!\n");
+ }
+
+ @Override
+ protected void succeeded(Description description) {
+ Log.i("TestWatcher", "*** "+description + " succeeded!\n");
+ }
+
+ protected void starting(Description description) {
+ Log.i("TestWatcher", "******\n*** "+description + " starting!\n");
+ runningTest = description.toString();
+ }
+
+ protected void finished(Description description) {
+ Log.i("TestWatcher", "*** "+description + " finished!\n******\n");
+ }
+ };
+
+ private void disconnectInternet() throws IOException {
+ UiDevice
+ .getInstance(androidx.test.InstrumentationRegistry.getInstrumentation())
+ .executeShellCommand("svc wifi disable"); // Switch off Wifi
+ UiDevice
+ .getInstance(androidx.test.InstrumentationRegistry.getInstrumentation())
+ .executeShellCommand("svc data disable"); // Switch off Mobile Data
+ }
+
+ private void connectInternet() throws IOException {
+ UiDevice
+ .getInstance(androidx.test.InstrumentationRegistry.getInstrumentation())
+ .executeShellCommand("svc wifi enable"); // Switch Wifi on again
+ UiDevice
+ .getInstance(androidx.test.InstrumentationRegistry.getInstrumentation())
+ .executeShellCommand("svc data enable"); // Switch Mobile Data on again
+ }
+
+ @Test
+ public void dynamicAddRemoveRendererTest() {
+ dynamicConferenceActivityScenarioRule.getScenario().onActivity(activity -> {
+ mIdlingResource = activity.getIdlingResource();
+ IdlingRegistry.getInstance().register(mIdlingResource);
+ activity.sendBroadcast(new Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS));
+ SurfaceViewRenderer renderer = null;
+ for(int i=0;i<10;i++)
+ renderer = activity.addSurfaceViewRenderer();
+
+ try {
+ Thread.sleep(300);
+ } catch (InterruptedException e) {
+ throw new RuntimeException(e);
+ }
+ GridLayout remoteParticipantsGridLayout = activity.findViewById(R.id.remote_participant_renderer);
+ assertEquals(remoteParticipantsGridLayout.getChildCount(),10);
+
+ activity.removeSurfaceViewRenderer(renderer);
+ assertEquals(remoteParticipantsGridLayout.getChildCount(),9);
+
+ activity.removeAllRenderers();
+ assertEquals(remoteParticipantsGridLayout.getChildCount(),0);
+ });
+ }
+
+}
\ No newline at end of file
diff --git a/webrtc-android-sample-app/src/main/AndroidManifest.xml b/webrtc-android-sample-app/src/main/AndroidManifest.xml
index 1c243fe3..b4fd0c46 100644
--- a/webrtc-android-sample-app/src/main/AndroidManifest.xml
+++ b/webrtc-android-sample-app/src/main/AndroidManifest.xml
@@ -67,6 +67,10 @@
android:exported="true"
android:theme="@style/Theme.AppCompat.DayNight"
android:configChanges="orientation|keyboard|screenSize|smallestScreenSize|screenLayout"/>
+
audioTrackStatItems = new ArrayList<>();
+ private ArrayList videoTrackStatItems = new ArrayList<>();
+
+ private TrackStatsAdapter audioTrackStatsAdapter;
+ private TrackStatsAdapter videoTrackStatsAdapter;
+
+ private boolean publishStarted = false;
+
+ /*
+ * We will receive videoTrack objects from the server through the onNewVideoTrack callback of the webrtc client listener.
+ * These videoTracks are not yet assigned to a streamId. We store them inside videoTrackList.
+ */
+ private ArrayList videoTrackList = new ArrayList<>();
+
+ /*
+ * Store track assignments received through the data channel from the server.
+ */
+ private JSONArray trackAssignments;
+
+ /*
+ * A data channel message will arrive containing the eventType VIDEO_TRACK_ASSIGNMENT_LIST.
+ * This message includes a videoLabel (trackId) and trackId (actual streamId).
+ * Upon receiving this message, we will match our videoTrack objects with the streamIds and store them in the map below.
+ * This allows us to determine which video track belongs to which stream id.
+ */
+ private HashMap streamIdVideoTrackMap = new HashMap<>();
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_dynamic_conference);
+ remoteParticipantsGridLayout = findViewById(R.id.remote_participant_renderer);
+ localParticipantRenderer = findViewById(R.id.local_participant_renderer);
+
+ statusIndicatorTextView = findViewById(R.id.broadcasting_text_view);
+ joinButton = findViewById(R.id.join_conference_button);
+
+ toggleSendAudioButton = findViewById(R.id.toggle_send_audio_button);
+ toggleSendVideoButton = findViewById(R.id.toggle_send_video_button);
+
+ serverUrl = sharedPreferences.getString(getString(R.string.serverAddress), SettingsActivity.DEFAULT_WEBSOCKET_URL);
+
+ roomId = sharedPreferences.getString(getString(R.string.roomId), SettingsActivity.DEFAULT_ROOM_NAME);
+ streamId = "streamId" + (int)(Math.random()*9999);
+
+ Switch playOnlySwitch = findViewById(R.id.play_only_switch);
+ playOnlySwitch.setOnCheckedChangeListener((compoundButton, b) -> {
+ playOnly = b;
+ localParticipantRenderer.setVisibility(b ? View.GONE : View.VISIBLE);
+ });
+
+ if(initBeforeStream){
+ if(PermissionHandler.checkCameraPermissions(this)){
+ createWebRTCClient();
+ }else{
+ PermissionHandler.requestCameraPermissions(this);
+ }
+ }else{
+ createWebRTCClient();
+ }
+
+ toggleSendVideoButton.setOnClickListener(v->{
+ toggleSendVideo();
+ });
+
+ toggleSendAudioButton.setOnClickListener(v->{
+ toggleSendAudio();
+ });
+
+ }
+
+ public void createWebRTCClient(){
+ webRTCClient = IWebRTCClient.builder()
+ .setLocalVideoRenderer(localParticipantRenderer)
+ .setServerUrl(serverUrl)
+ .setActivity(this)
+ .setVideoCallEnabled(videoCallEnabled)
+ .setAudioCallEnabled(audioCallEnabled)
+ .setInitiateBeforeStream(initBeforeStream)
+ .setBluetoothEnabled(bluetoothEnabled)
+ .setWebRTCListener(createWebRTCListener(roomId, streamId))
+ .setDataChannelObserver(createDatachannelObserver())
+ .build();
+
+ joinButton = findViewById(R.id.join_conference_button);
+
+ joinButton.setOnClickListener(v -> {
+ joinLeaveRoom();
+ });
+
+ ImageButton showStatsButton = findViewById(R.id.show_stats_button);
+ showStatsButton.setOnClickListener(v -> {
+
+ if(publishStarted){
+ showStatsPopup();
+ }else{
+ runOnUiThread(() -> {
+ Toast.makeText(DynamicConferenceActivity.this,"Start publishing first.", Toast.LENGTH_SHORT).show();
+ });
+ }
+ });
+
+ }
+
+
+ public void joinLeaveRoom() {
+ if(!initBeforeStream) {
+ if (!PermissionHandler.checkCameraPermissions(this)) {
+ PermissionHandler.requestCameraPermissions(this);
+ return;
+ }else if(!PermissionHandler.checkPublishPermissions(this, bluetoothEnabled)){
+ PermissionHandler.requestPublishPermissions(this, bluetoothEnabled);
+ return;
+ }
+ }
+
+ if (!webRTCClient.isStreaming(streamId)) {
+ joinButton.setEnabled(false);
+ joinButton.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.ic_end_call));
+ Log.i(getClass().getSimpleName(), "Calling join");
+
+ if(playOnly) {
+ webRTCClient.joinToConferenceRoom(roomId);
+ }
+ else {
+ webRTCClient.joinToConferenceRoom(roomId, streamId, videoCallEnabled, audioCallEnabled, "", "", "", "");
+ }
+
+ }
+ else {
+ joinButton.setEnabled(false);
+ joinButton.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.ic_join_call));
+ Log.i(getClass().getSimpleName(), "Calling leave");
+
+ webRTCClient.leaveFromConference(roomId);
+ removeAllRenderers();
+ }
+ }
+
+ private IDataChannelObserver createDatachannelObserver() {
+ return new DefaultDataChannelObserver() {
+ @Override
+ public void textMessageReceived(String messageText) {
+ super.textMessageReceived(messageText);
+ try{
+ JSONObject msgJsonObj = new JSONObject(messageText);
+ if(msgJsonObj.has(DataChannelConstants.EVENT_TYPE) && msgJsonObj.getString(DataChannelConstants.EVENT_TYPE).equals(DataChannelConstants.VIDEO_TRACK_ASSIGNMENT_LIST)){
+ trackAssignments = msgJsonObj.getJSONArray(DataChannelConstants.PAYLOAD);
+ matchStreamIdAndVideoTrack();
+ }
+ }catch (Exception e){
+ Log.e(getClass().getSimpleName(),"Cant parse data channel message to JSON object. "+e.getMessage());
+ }
+ }
+ };
+ }
+
+ private void matchStreamIdAndVideoTrack(){
+ try{
+ for(int i=0;i {
+ removeSurfaceViewRenderer(r);
+ });
+ return;
+ }
+ }
+ }
+ @Override
+ public void onNewVideoTrack(VideoTrack track, String trackId) {
+ String messageText = "New video track received";
+ callbackCalled(messageText);
+
+ runOnUiThread(() -> {
+ SurfaceViewRenderer r = addSurfaceViewRenderer();
+ if (r.getTag() == null) {
+ r.setTag(track);
+ webRTCClient.setRendererForVideoTrack(r, track);
+ }
+ videoTrackList.add(track);
+ if(trackAssignments != null){
+ matchStreamIdAndVideoTrack();
+ }
+ });
+
+ }
+
+ @Override
+ public void onPublishFinished(String streamId) {
+ super.onPublishFinished(streamId);
+ decrementIdle();
+ }
+ };
+ }
+
+ public void toggleSendVideo() {
+ if(webRTCClient.isShutdown()){
+ Toast.makeText(this, "Webrtc client is shutdown. Recreate it.", Toast.LENGTH_LONG).show();
+ return;
+ }
+
+ if(webRTCClient.getConfig().videoCallEnabled){
+ if(webRTCClient.isSendVideoEnabled()){
+ webRTCClient.toggleSendVideo(false);
+ toggleSendVideoButton.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.ic_camera_off));
+
+ }else{
+ webRTCClient.toggleSendVideo(true);
+ toggleSendVideoButton.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.ic_camera_on));
+ }
+ }else{
+ Toast.makeText(this, "Cannot toggle send video because its disabled in config", Toast.LENGTH_LONG).show();
+ }
+ }
+
+ public void toggleSendAudio() {
+ if(webRTCClient.getConfig().audioCallEnabled){
+ if(webRTCClient.isSendAudioEnabled()){
+ webRTCClient.toggleSendAudio(false);
+ toggleSendAudioButton.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.ic_mic_off));
+
+ }else{
+ webRTCClient.toggleSendAudio(true);
+ toggleSendAudioButton.setImageDrawable(ContextCompat.getDrawable(getApplicationContext(), R.drawable.ic_mic_on));
+ }
+ }else{
+ Toast.makeText(this, "Cannot toggle send audio because its disabled in config", Toast.LENGTH_LONG).show();
+ }
+ }
+
+ private void showStatsPopup() {
+ LayoutInflater li = LayoutInflater.from(this);
+
+ View promptsView = li.inflate(R.layout.multitrack_stats_popup, null);
+
+ AlertDialog.Builder alertDialogBuilder = new AlertDialog.Builder(this);
+
+ alertDialogBuilder.setView(promptsView);
+
+ alertDialogBuilder.setCancelable(true);
+ statsPopup = alertDialogBuilder.create();
+
+
+ TextView packetsLostAudio = promptsView.findViewById(R.id.multitrack_stats_popup_packets_lost_audio_textview);
+ TextView jitterAudio = promptsView.findViewById(R.id.multitrack_stats_popup_jitter_audio_textview);
+ TextView rttAudio = promptsView.findViewById(R.id.multitrack_stats_popup_rtt_audio_textview);
+ TextView packetLostRatioAudio = promptsView.findViewById(R.id.multitrack_stats_popup_packet_lost_ratio_audio_textview);
+ TextView firCountAudio = promptsView.findViewById(R.id.multitrack_stats_popup_fir_count_audio_textview);
+ TextView pliCountAudio = promptsView.findViewById(R.id.multitrack_stats_popup_pli_count_audio_textview);
+ TextView nackCountAudio = promptsView.findViewById(R.id.multitrack_stats_popup_nack_count_audio_textview);
+ TextView packetsSentAudio = promptsView.findViewById(R.id.multitrack_stats_popup_packets_sent_audio_textview);
+ TextView framesEncodedAudio = promptsView.findViewById(R.id.multitrack_stats_popup_frames_encoded_audio_textview);
+ TextView bytesSentAudio = promptsView.findViewById(R.id.multitrack_stats_popup_bytes_sent_audio_textview);
+ TextView packetsSentPerSecondAudio = promptsView.findViewById(R.id.multitrack_stats_popup_packets_sent_per_second_audio_textview);
+ TextView localAudioBitrate = promptsView.findViewById(R.id.multitrack_stats_popup_local_audio_bitrate_textview);
+ TextView localAudioLevel = promptsView.findViewById(R.id.multitrack_stats_popup_local_audio_level_textview);
+
+ TextView packetsLostVideo = promptsView.findViewById(R.id.multitrack_stats_popup_packets_lost_video_textview);
+ TextView jitterVideo = promptsView.findViewById(R.id.multitrack_stats_popup_jitter_video_textview);
+ TextView rttVideo = promptsView.findViewById(R.id.multitrack_stats_popup_rtt_video_textview);
+ TextView packetLostRatioVideo = promptsView.findViewById(R.id.multitrack_stats_popup_packet_lost_ratio_video_textview);
+ TextView firCountVideo = promptsView.findViewById(R.id.multitrack_stats_popup_fir_count_video_textview);
+ TextView pliCountVideo = promptsView.findViewById(R.id.multitrack_stats_popup_pli_count_video_textview);
+ TextView nackCountVideo = promptsView.findViewById(R.id.multitrack_stats_popup_nack_count_video_textview);
+ TextView packetsSentVideo = promptsView.findViewById(R.id.multitrack_stats_popup_packets_sent_video_textview);
+ TextView framesEncodedVideo = promptsView.findViewById(R.id.multitrack_stats_popup_frames_encoded_video_textview);
+ TextView bytesSentVideo = promptsView.findViewById(R.id.multitrack_stats_popup_bytes_sent_video_textview);
+ TextView packetsSentPerSecondVideo = promptsView.findViewById(R.id.multitrack_stats_popup_packets_sent_per_second_video_textview);
+ TextView localVideoBitrate = promptsView.findViewById(R.id.multitrack_stats_popup_local_video_bitrate_textview);
+
+ audioTrackStatsAdapter = new TrackStatsAdapter(audioTrackStatItems, this);
+ videoTrackStatsAdapter = new TrackStatsAdapter(videoTrackStatItems, this);
+
+ RecyclerView playStatsAudioTrackRecyclerview = promptsView.findViewById(R.id.multitrack_stats_popup_play_stats_audio_track_recyclerview);
+ RecyclerView playStatsVideoTrackRecyclerview = promptsView.findViewById(R.id.multitrack_stats_popup_play_stats_video_track_recyclerview);
+
+ LinearLayoutManager linearLayoutManager1 = new LinearLayoutManager(this);
+ LinearLayoutManager linearLayoutManager2 = new LinearLayoutManager(this);
+
+ playStatsAudioTrackRecyclerview.setLayoutManager(linearLayoutManager1);
+ playStatsVideoTrackRecyclerview.setLayoutManager(linearLayoutManager2);
+
+
+ playStatsAudioTrackRecyclerview.setAdapter(audioTrackStatsAdapter);
+ playStatsVideoTrackRecyclerview.setAdapter(videoTrackStatsAdapter);
+
+ Button closeButton = promptsView.findViewById(R.id.multitrack_stats_popup_close_button);
+
+ closeButton.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ statsPopup.dismiss();
+ }
+ });
+
+ statCollectorExecutor = Executors.newScheduledThreadPool(1);
+ statCollectorFuture = statCollectorExecutor.scheduleWithFixedDelay(() -> {
+ runOnUiThread(() -> {
+ try {
+ TrackStats audioTrackStats = webRTCClient.getStatsCollector().getPublishStats().getAudioTrackStats();
+ packetsLostAudio.setText(String.valueOf(audioTrackStats.getPacketsLost()));
+ jitterAudio.setText(String.valueOf(audioTrackStats.getJitter()));
+ rttAudio.setText(String.valueOf(audioTrackStats.getRoundTripTime()));
+ packetLostRatioAudio.setText(String.valueOf(audioTrackStats.getPacketLostRatio()));
+ firCountAudio.setText(String.valueOf(audioTrackStats.getFirCount()));
+ pliCountAudio.setText(String.valueOf(audioTrackStats.getPliCount()));
+ nackCountAudio.setText(String.valueOf(audioTrackStats.getNackCount()));
+ packetsSentAudio.setText(String.valueOf(audioTrackStats.getPacketsSent()));
+ framesEncodedAudio.setText(String.valueOf(audioTrackStats.getFramesEncoded()));
+ bytesSentAudio.setText(String.valueOf(audioTrackStats.getBytesSent()));
+ packetsSentPerSecondAudio.setText(String.valueOf(audioTrackStats.getPacketsSentPerSecond()));
+ packetsSentAudio.setText(String.valueOf(audioTrackStats.getPacketsSent()));
+
+ TrackStats videoTrackStats = webRTCClient.getStatsCollector().getPublishStats().getVideoTrackStats();
+ packetsLostVideo.setText(String.valueOf(videoTrackStats.getPacketsLost()));
+ jitterVideo.setText(String.valueOf(videoTrackStats.getJitter()));
+ rttVideo.setText(String.valueOf(videoTrackStats.getRoundTripTime()));
+ packetLostRatioVideo.setText(String.valueOf(videoTrackStats.getPacketLostRatio()));
+ firCountVideo.setText(String.valueOf(videoTrackStats.getFirCount()));
+ pliCountVideo.setText(String.valueOf(videoTrackStats.getPliCount()));
+ nackCountVideo.setText(String.valueOf(videoTrackStats.getNackCount()));
+ packetsSentVideo.setText(String.valueOf(videoTrackStats.getPacketsSent()));
+ framesEncodedVideo.setText(String.valueOf(videoTrackStats.getFramesEncoded()));
+ bytesSentVideo.setText(String.valueOf(videoTrackStats.getBytesSent()));
+ packetsSentPerSecondVideo.setText(String.valueOf(videoTrackStats.getPacketsSentPerSecond()));
+ packetsSentVideo.setText(String.valueOf(videoTrackStats.getPacketsSent()));
+
+ localAudioBitrate.setText(String.valueOf(webRTCClient.getStatsCollector().getPublishStats().getAudioBitrate()));
+ localAudioLevel.setText(String.valueOf(webRTCClient.getStatsCollector().getPublishStats().getLocalAudioLevel()));
+ localVideoBitrate.setText(String.valueOf(webRTCClient.getStatsCollector().getPublishStats().getVideoBitrate()));
+
+
+ PlayStats playStats = webRTCClient.getStatsCollector().getPlayStats();
+ audioTrackStatItems.clear();
+ audioTrackStatItems.addAll(playStats.getAudioTrackStatsMap().values());
+
+ videoTrackStatItems.clear();
+ videoTrackStatItems.addAll(playStats.getVideoTrackStatsMap().values());
+
+ audioTrackStatsAdapter.notifyDataSetChanged();
+ videoTrackStatsAdapter.notifyDataSetChanged();
+
+ } catch (Exception e) {
+ Log.e("DynamicConference", "Exception in task execution: " + e.getMessage());
+ }
+ });
+
+ }, 0, UPDATE_STATS_INTERVAL_MS, TimeUnit.MILLISECONDS);
+
+
+ statsPopup.setOnDismissListener(dialog -> {
+ if (statCollectorFuture != null && !statCollectorFuture.isCancelled()) {
+ statCollectorFuture.cancel(true);
+ }
+ if (statCollectorExecutor != null && !statCollectorExecutor.isShutdown()) {
+ statCollectorExecutor.shutdown();
+ }
+ });
+
+ statsPopup.show();
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+ if(requestCode == PermissionHandler.CAMERA_PERMISSION_REQUEST_CODE){
+ boolean allPermissionsGranted = true;
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ allPermissionsGranted = false;
+ break;
+ }
+ }
+ if (initBeforeStream && allPermissionsGranted) {
+ createWebRTCClient();
+ }else if(!initBeforeStream && allPermissionsGranted){
+ joinLeaveRoom();
+ }
+ else {
+ Toast.makeText(this,"Camera permissions are not granted. Cannot initialize.", Toast.LENGTH_LONG).show();
+ }
+
+
+ }else if(requestCode == PermissionHandler.PUBLISH_PERMISSION_REQUEST_CODE){
+
+ boolean allPermissionsGranted = true;
+ for (int result : grantResults) {
+ if (result != PackageManager.PERMISSION_GRANTED) {
+ allPermissionsGranted = false;
+ break;
+ }
+ }
+
+ if (allPermissionsGranted) {
+ joinLeaveRoom();
+ } else {
+ Toast.makeText(this,"Publish permissions are not granted.", Toast.LENGTH_LONG).show();
+ }
+ }
+ }
+ public SurfaceViewRenderer createSurfaceViewRender(){
+ SurfaceViewRenderer surfaceViewRenderer = new SurfaceViewRenderer(this);
+
+ GridLayout.LayoutParams params = new GridLayout.LayoutParams();
+ params.width = (500);
+ params.height = (500);
+ params.setMargins(8, 8, 8, 8);
+
+ surfaceViewRenderer.setLayoutParams(params);
+ return surfaceViewRenderer;
+ }
+ public SurfaceViewRenderer addSurfaceViewRenderer() {
+ SurfaceViewRenderer surfaceViewRenderer = createSurfaceViewRender();
+ remoteParticipantsGridLayout.addView(surfaceViewRenderer);
+ webRTCClient.getConfig().remoteVideoRenderers.add(surfaceViewRenderer);
+ return surfaceViewRenderer;
+ }
+ public void removeSurfaceViewRenderer(SurfaceViewRenderer renderer){
+ if(renderer==null)
+ return;
+ remoteParticipantsGridLayout.removeView(renderer);
+ webRTCClient.getConfig().remoteVideoRenderers.remove(renderer);
+ }
+ public void removeAllRenderers(){
+ ArrayList toRemove = new ArrayList<>(webRTCClient.getConfig().remoteVideoRenderers);
+ for (SurfaceViewRenderer r : toRemove) {
+ webRTCClient.releaseRenderer(r);
+ runOnUiThread(() -> {
+ remoteParticipantsGridLayout.removeView(r);
+ webRTCClient.getConfig().remoteVideoRenderers.remove(r);
+ });
+ }
+ }
+}
diff --git a/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_camera_off.xml b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_camera_off.xml
new file mode 100644
index 00000000..10370809
--- /dev/null
+++ b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_camera_off.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_camera_on.xml b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_camera_on.xml
new file mode 100644
index 00000000..4e12a8fb
--- /dev/null
+++ b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_camera_on.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_end_call.xml b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_end_call.xml
new file mode 100644
index 00000000..85eb73d2
--- /dev/null
+++ b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_end_call.xml
@@ -0,0 +1,12 @@
+
+
+
+
diff --git a/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_join_call.xml b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_join_call.xml
new file mode 100644
index 00000000..bbd8e99f
--- /dev/null
+++ b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_join_call.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_mic_off.xml b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_mic_off.xml
new file mode 100644
index 00000000..b998a1d6
--- /dev/null
+++ b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_mic_off.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_mic_on.xml b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_mic_on.xml
new file mode 100644
index 00000000..c0b1f056
--- /dev/null
+++ b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_mic_on.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_query_stats.xml b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_query_stats.xml
new file mode 100644
index 00000000..d91aa28e
--- /dev/null
+++ b/webrtc-android-sample-app/src/main/res/drawable-anydpi/ic_query_stats.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_camera_off.png b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_camera_off.png
new file mode 100644
index 00000000..3b16b43b
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_camera_off.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_camera_on.png b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_camera_on.png
new file mode 100644
index 00000000..08a33c0d
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_camera_on.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_end_call.png b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_end_call.png
new file mode 100644
index 00000000..36726042
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_end_call.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_join_call.png b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_join_call.png
new file mode 100644
index 00000000..e6ef4704
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_join_call.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_mic_off.png b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_mic_off.png
new file mode 100644
index 00000000..42107c35
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_mic_off.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_mic_on.png b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_mic_on.png
new file mode 100644
index 00000000..6d384efc
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_mic_on.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_query_stats.png b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_query_stats.png
new file mode 100644
index 00000000..1d9f574b
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-hdpi/ic_query_stats.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_camera_off.png b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_camera_off.png
new file mode 100644
index 00000000..16ba6913
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_camera_off.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_camera_on.png b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_camera_on.png
new file mode 100644
index 00000000..e1a28eea
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_camera_on.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_end_call.png b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_end_call.png
new file mode 100644
index 00000000..f2de3e4f
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_end_call.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_join_call.png b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_join_call.png
new file mode 100644
index 00000000..5d990ece
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_join_call.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_mic_on.png b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_mic_on.png
new file mode 100644
index 00000000..b6d8e780
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_mic_on.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_query_stats.png b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_query_stats.png
new file mode 100644
index 00000000..19bbd1e8
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-mdpi/ic_query_stats.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_camera_off.png b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_camera_off.png
new file mode 100644
index 00000000..bd44a4be
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_camera_off.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_camera_on.png b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_camera_on.png
new file mode 100644
index 00000000..2bbb6c26
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_camera_on.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_end_call.png b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_end_call.png
new file mode 100644
index 00000000..46ad59a4
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_end_call.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_join_call.png b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_join_call.png
new file mode 100644
index 00000000..89f1afda
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_join_call.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_mic_off.png b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_mic_off.png
new file mode 100644
index 00000000..668e454a
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_mic_off.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_mic_on.png b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_mic_on.png
new file mode 100644
index 00000000..0138dec7
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_mic_on.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_query_stats.png b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_query_stats.png
new file mode 100644
index 00000000..9e64ded2
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xhdpi/ic_query_stats.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_camera_off.png b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_camera_off.png
new file mode 100644
index 00000000..943f56a1
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_camera_off.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_camera_on.png b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_camera_on.png
new file mode 100644
index 00000000..93c03728
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_camera_on.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_end_call.png b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_end_call.png
new file mode 100644
index 00000000..de0c1754
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_end_call.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_join_call.png b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_join_call.png
new file mode 100644
index 00000000..7c95467a
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_join_call.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_mic_off.png b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_mic_off.png
new file mode 100644
index 00000000..60a35978
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_mic_off.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_mic_on.png b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_mic_on.png
new file mode 100644
index 00000000..f9fc5bc5
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_mic_on.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_query_stats.png b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_query_stats.png
new file mode 100644
index 00000000..e58b4775
Binary files /dev/null and b/webrtc-android-sample-app/src/main/res/drawable-xxhdpi/ic_query_stats.png differ
diff --git a/webrtc-android-sample-app/src/main/res/drawable/rounded_button_background.xml b/webrtc-android-sample-app/src/main/res/drawable/rounded_button_background.xml
new file mode 100644
index 00000000..d6c8994c
--- /dev/null
+++ b/webrtc-android-sample-app/src/main/res/drawable/rounded_button_background.xml
@@ -0,0 +1,9 @@
+
+ -
+
+
+
+
+
+
\ No newline at end of file
diff --git a/webrtc-android-sample-app/src/main/res/layout/activity_dynamic_conference.xml b/webrtc-android-sample-app/src/main/res/layout/activity_dynamic_conference.xml
new file mode 100644
index 00000000..70205f04
--- /dev/null
+++ b/webrtc-android-sample-app/src/main/res/layout/activity_dynamic_conference.xml
@@ -0,0 +1,121 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file