From 984a50d2d7f5eba0eb2d40e8b4d027cd53917bf8 Mon Sep 17 00:00:00 2001 From: USAMAWIZARD Date: Tue, 21 Jan 2025 16:36:23 +0530 Subject: [PATCH 1/9] dynamically adding and removing renderer in Conference --- .../api/IWebRTCClient.java | 8 + .../api/WebRTCClientBuilder.java | 7 + .../api/WebRTCClientConfig.java | 5 + .../core/WebRTCClient.java | 23 +- .../src/main/AndroidManifest.xml | 4 + .../MainActivity.java | 3 +- .../basic/DynamicConferenceActivity.java | 556 ++++++++++++++++++ .../layout/activity_dynamic_conference.xml | 94 +++ 8 files changed, 698 insertions(+), 2 deletions(-) create mode 100644 webrtc-android-sample-app/src/main/java/io/antmedia/webrtc_android_sample_app/basic/DynamicConferenceActivity.java create mode 100644 webrtc-android-sample-app/src/main/res/layout/activity_dynamic_conference.xml diff --git a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/IWebRTCClient.java b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/IWebRTCClient.java index 78362306..41409c1c 100644 --- a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/IWebRTCClient.java +++ b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/IWebRTCClient.java @@ -368,4 +368,12 @@ void publish(String streamId, String token, boolean videoCallEnabled, boolean au */ boolean isSendVideoEnabled(); + /** + * add new surfaceViewRenderer + * + */ + public SurfaceViewRenderer addSurfaceViewRenderer(String id); + + public void removeSurfaceViewRenderer(SurfaceViewRenderer surfaceViewRenderer); } + diff --git a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientBuilder.java b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientBuilder.java index c022b3a8..2fa6fb74 100644 --- a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientBuilder.java +++ b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientBuilder.java @@ -1,6 +1,7 @@ package io.antmedia.webrtcandroidframework.api; import android.app.Activity; +import android.widget.GridLayout; import org.webrtc.RendererCommon; import org.webrtc.SurfaceViewRenderer; @@ -95,6 +96,12 @@ public WebRTCClientBuilder addRemoteVideoRenderer (SurfaceViewRenderer ... remot webRTCClientConfig.remoteVideoRenderers.addAll(Arrays.asList(remoteVideoRenderers)); return this; } + public WebRTCClientBuilder useDynamicRenders(GridLayout remoteParticipatesGrid, int maxVideosInGrid) { + webRTCClientConfig.useDynamicRenderers = true; + webRTCClientConfig.remoteParticipantsGridLayout = remoteParticipatesGrid; + webRTCClientConfig.maxVideosInGrid = maxVideosInGrid; + return this; + } public WebRTCClientBuilder setWebRTCListener(IWebRTCListener webRTCListener) { webRTCClientConfig.webRTCListener = webRTCListener; diff --git a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientConfig.java b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientConfig.java index 531f6abb..6d63a639 100644 --- a/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientConfig.java +++ b/webrtc-android-framework/src/main/java/io/antmedia/webrtcandroidframework/api/WebRTCClientConfig.java @@ -3,6 +3,7 @@ import android.app.Activity; import android.content.Intent; import android.media.projection.MediaProjection; +import android.widget.GridLayout; import org.webrtc.RendererCommon; import org.webrtc.SurfaceViewRenderer; @@ -17,10 +18,14 @@ public class WebRTCClientConfig { */ public SurfaceViewRenderer localVideoRenderer; + public boolean useDynamicRenderers = true; /* * Renderers for remote video */ public ArrayList remoteVideoRenderers = new ArrayList<>(); + public GridLayout remoteParticipantsGridLayout; + + public int maxVideosInGrid; /* * websocket connection url to Ant Media Server 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..795e03ab 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 @@ -15,7 +15,9 @@ import android.os.Handler; import android.util.DisplayMetrics; import android.util.Log; +import android.view.ViewGroup; import android.view.WindowManager; +import android.widget.GridLayout; import android.widget.Toast; import androidx.annotation.NonNull; @@ -404,7 +406,7 @@ public WebRTCClient(WebRTCClientConfig config) { .createIceServer()); } - if (config.initiateBeforeStream) { + if (config.initiateBeforeStream = true) { init(); } } @@ -764,6 +766,7 @@ public void initializeRenderers() { config.localVideoRenderer.setEnableHardwareScaler(true /* enabled */); localVideoSink.setTarget(config.localVideoRenderer); } + } public void initializePeerConnectionFactory() { @@ -2866,5 +2869,23 @@ public AudioTrack getLocalAudioTrack() { public void setLocalAudioTrack(@androidx.annotation.Nullable AudioTrack localAudioTrack) { this.localAudioTrack = localAudioTrack; } + public SurfaceViewRenderer addSurfaceViewRenderer(String id) { + SurfaceViewRenderer surfaceViewRenderer = new SurfaceViewRenderer(config.activity); + + GridLayout.LayoutParams params = new GridLayout.LayoutParams(); + params.width = (500); + params.height = (500); + params.setMargins(8, 8, 8, 8); + + surfaceViewRenderer.setLayoutParams(params); + config.remoteParticipantsGridLayout.addView(surfaceViewRenderer); + config.remoteVideoRenderers.add(surfaceViewRenderer); + return surfaceViewRenderer; + } + public void removeSurfaceViewRenderer(SurfaceViewRenderer renderer){ + config.remoteParticipantsGridLayout.removeView(renderer); + } + private void removeSurfaceViewRenderer(String id){ + } } \ 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); + remoteParticipatesGrid = 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() + .useDynamicRenders(remoteParticipatesGrid,10) + .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(); + }); + + Button 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.setText("Leave"); + Log.i(getClass().getSimpleName(), "Calling join"); + + if(playOnly) { + webRTCClient.joinToConferenceRoom(roomId); + } + else { + webRTCClient.joinToConferenceRoom(roomId, streamId, videoCallEnabled, audioCallEnabled, "", "", "", ""); + } + + } + else { + joinButton.setText("Join"); + Log.i(getClass().getSimpleName(), "Calling leave"); + + webRTCClient.leaveFromConference(roomId); + } + } + + 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 { + webRTCClient.removeSurfaceViewRenderer(r); + }); + return; + } + } + } + @Override + public void onNewVideoTrack(VideoTrack track, String trackId) { + String messageText = "New video track received"; + callbackCalled(messageText); + + runOnUiThread(() -> { + SurfaceViewRenderer r = webRTCClient.addSurfaceViewRenderer(trackId); + 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.setText("Enable Send Video"); + + }else{ + webRTCClient.toggleSendVideo(true); + toggleSendVideoButton.setText("Disable Send Video"); + } + }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.setText("Enable Send Audio"); + + }else{ + webRTCClient.toggleSendAudio(true); + toggleSendAudioButton.setText("Disable Send Audio"); + + } + }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(); + } + } + } + +} 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..0f2f242d --- /dev/null +++ b/webrtc-android-sample-app/src/main/res/layout/activity_dynamic_conference.xml @@ -0,0 +1,94 @@ + + + + + + + + + + + + + + + + + + + + + +