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