add button to switch cameras during video call
RIP symmetry :-( fixes #3683
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" viewBox="0 0 24 24" fill="black" width="24px" height="24px"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M9,12c0,1.66,1.34,3,3,3s3-1.34,3-3s-1.34-3-3-3S9,10.34,9,12z"/><path d="M8,10V8H5.09C6.47,5.61,9.05,4,12,4c3.72,0,6.85,2.56,7.74,6h2.06c-0.93-4.56-4.96-8-9.8-8C8.73,2,5.82,3.58,4,6.01V4H2v6 H8z"/><path d="M16,14v2h2.91c-1.38,2.39-3.96,4-6.91,4c-3.72,0-6.85-2.56-7.74-6H2.2c0.93,4.56,4.96,8,9.8,8c3.27,0,6.18-1.58,8-4.01V20 h2v-6H16z"/></g></g></svg>
|
After Width: | Height: | Size: 547 B |
|
@ -27,6 +27,7 @@ images = {
|
|||
'open_pdf_white.svg' => ['open_pdf_white', 128],
|
||||
'conversations_mono.svg' => ['conversations/ic_notification', 24],
|
||||
'quicksy_mono.svg' => ['quicksy/ic_notification', 24],
|
||||
'flip_camera_android-black-24dp.svg' => ['ic_flip_camera_android_black_24dp', 24],
|
||||
'ic_send_text_offline.svg' => ['ic_send_text_offline', 36],
|
||||
'ic_send_text_offline_white.svg' => ['ic_send_text_offline_white', 36],
|
||||
'ic_send_text_online.svg' => ['ic_send_text_online', 36],
|
||||
|
|
|
@ -22,9 +22,14 @@ import android.widget.Toast;
|
|||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.base.Throwables;
|
||||
import com.google.common.collect.ImmutableList;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.util.concurrent.FutureCallback;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.checkerframework.checker.nullness.compatqual.NullableDecl;
|
||||
import org.webrtc.SurfaceViewRenderer;
|
||||
import org.webrtc.VideoTrack;
|
||||
|
||||
|
@ -42,6 +47,7 @@ import eu.siacs.conversations.entities.Contact;
|
|||
import eu.siacs.conversations.services.AppRTCAudioManager;
|
||||
import eu.siacs.conversations.services.XmppConnectionService;
|
||||
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
|
||||
import eu.siacs.conversations.ui.util.MainThreadExecutor;
|
||||
import eu.siacs.conversations.utils.PermissionUtils;
|
||||
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
|
||||
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
|
||||
|
@ -83,7 +89,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
Log.d(Config.LOGTAG, this.getClass().getName() + ".onCreate()");
|
||||
super.onCreate(savedInstanceState);
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
|
||||
| WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
|
||||
|
@ -561,18 +566,21 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) {
|
||||
Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
|
||||
if (media.contains(Media.VIDEO)) {
|
||||
updateInCallButtonConfigurationVideo(requireRtpConnection().isVideoEnabled());
|
||||
final JingleRtpConnection rtpConnection = requireRtpConnection();
|
||||
updateInCallButtonConfigurationVideo(rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
|
||||
} else {
|
||||
final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
|
||||
updateInCallButtonConfigurationSpeaker(
|
||||
audioManager.getSelectedAudioDevice(),
|
||||
audioManager.getAudioDevices().size()
|
||||
);
|
||||
this.binding.inCallActionFarRight.setVisibility(View.GONE);
|
||||
}
|
||||
updateInCallButtonConfigurationMicrophone(requireRtpConnection().isMicrophoneEnabled());
|
||||
} else {
|
||||
this.binding.inCallActionLeft.setVisibility(View.GONE);
|
||||
this.binding.inCallActionRight.setVisibility(View.GONE);
|
||||
this.binding.inCallActionFarRight.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -612,8 +620,15 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
}
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
private void updateInCallButtonConfigurationVideo(final boolean videoEnabled) {
|
||||
private void updateInCallButtonConfigurationVideo(final boolean videoEnabled, final boolean isCameraSwitchable) {
|
||||
this.binding.inCallActionRight.setVisibility(View.VISIBLE);
|
||||
if (isCameraSwitchable) {
|
||||
this.binding.inCallActionFarRight.setImageResource(R.drawable.ic_flip_camera_android_black_24dp);
|
||||
this.binding.inCallActionFarRight.setVisibility(View.VISIBLE);
|
||||
this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera);
|
||||
} else {
|
||||
this.binding.inCallActionFarRight.setVisibility(View.GONE);
|
||||
}
|
||||
if (videoEnabled) {
|
||||
this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_black_24dp);
|
||||
this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
|
||||
|
@ -623,14 +638,29 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
|
|||
}
|
||||
}
|
||||
|
||||
private void switchCamera(final View view) {
|
||||
Futures.addCallback(requireRtpConnection().switchCamera(), new FutureCallback<Void>() {
|
||||
@Override
|
||||
public void onSuccess(@NullableDecl Void result) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(final Throwable throwable) {
|
||||
Log.d(Config.LOGTAG,"could not switch camera", Throwables.getRootCause(throwable));
|
||||
Toast.makeText(RtpSessionActivity.this, R.string.could_not_switch_camera, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}, MainThreadExecutor.getInstance());
|
||||
}
|
||||
|
||||
private void enableVideo(View view) {
|
||||
requireRtpConnection().setVideoEnabled(true);
|
||||
updateInCallButtonConfigurationVideo(true);
|
||||
updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
|
||||
}
|
||||
|
||||
private void disableVideo(View view) {
|
||||
requireRtpConnection().setVideoEnabled(false);
|
||||
updateInCallButtonConfigurationVideo(false);
|
||||
updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable());
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright 2019 Daniel Gultsch
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package eu.siacs.conversations.ui.util;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
public class MainThreadExecutor implements Executor {
|
||||
|
||||
private static final MainThreadExecutor INSTANCE = new MainThreadExecutor();
|
||||
|
||||
private final Handler handler = new Handler(Looper.myLooper());
|
||||
|
||||
@Override
|
||||
public void execute(final Runnable command) {
|
||||
handler.post(command);
|
||||
}
|
||||
|
||||
public static MainThreadExecutor getInstance() {
|
||||
return INSTANCE;
|
||||
}
|
||||
}
|
|
@ -12,6 +12,7 @@ import com.google.common.collect.ImmutableList;
|
|||
import com.google.common.collect.ImmutableMap;
|
||||
import com.google.common.collect.Sets;
|
||||
import com.google.common.primitives.Ints;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.IceCandidate;
|
||||
|
@ -1037,6 +1038,15 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
|
|||
return webRTCWrapper.isVideoEnabled();
|
||||
}
|
||||
|
||||
|
||||
public boolean isCameraSwitchable() {
|
||||
return webRTCWrapper.isCameraSwitchable();
|
||||
}
|
||||
|
||||
public ListenableFuture<Void> switchCamera() {
|
||||
return webRTCWrapper.switchCamera();
|
||||
}
|
||||
|
||||
public void setVideoEnabled(final boolean enabled) {
|
||||
webRTCWrapper.setVideoEnabled(enabled);
|
||||
}
|
||||
|
|
|
@ -8,6 +8,8 @@ import android.util.Log;
|
|||
|
||||
import com.google.common.base.Optional;
|
||||
import com.google.common.base.Preconditions;
|
||||
import com.google.common.collect.ImmutableSet;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.util.concurrent.Futures;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
import com.google.common.util.concurrent.MoreExecutors;
|
||||
|
@ -255,7 +257,7 @@ public class WebRTCWrapper {
|
|||
try {
|
||||
peerConnection.dispose();
|
||||
} catch (final IllegalStateException e) {
|
||||
Log.e(Config.LOGTAG,"unable to dispose of peer connection", e);
|
||||
Log.e(Config.LOGTAG, "unable to dispose of peer connection", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -270,6 +272,31 @@ public class WebRTCWrapper {
|
|||
}
|
||||
}
|
||||
|
||||
boolean isCameraSwitchable() {
|
||||
final CapturerChoice capturerChoice = this.capturerChoice;
|
||||
return capturerChoice != null && capturerChoice.availableCameras.size() > 1;
|
||||
}
|
||||
|
||||
ListenableFuture<Void> switchCamera() {
|
||||
final CapturerChoice capturerChoice = this.capturerChoice;
|
||||
if (capturerChoice == null) {
|
||||
return Futures.immediateFailedFuture(new IllegalStateException("CameraCapturer has not been initialized"));
|
||||
}
|
||||
final SettableFuture<Void> future = SettableFuture.create();
|
||||
capturerChoice.cameraVideoCapturer.switchCamera(new CameraVideoCapturer.CameraSwitchHandler() {
|
||||
@Override
|
||||
public void onCameraSwitchDone(boolean isFrontCamera) {
|
||||
future.set(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraSwitchError(final String message) {
|
||||
future.setException(new IllegalStateException(String.format("Unable to switch camera %s", message)));
|
||||
}
|
||||
});
|
||||
return future;
|
||||
}
|
||||
|
||||
boolean isMicrophoneEnabled() {
|
||||
final AudioTrack audioTrack = this.localAudioTrack;
|
||||
if (audioTrack == null) {
|
||||
|
@ -408,21 +435,21 @@ public class WebRTCWrapper {
|
|||
|
||||
private Optional<CapturerChoice> getVideoCapturer() {
|
||||
final CameraEnumerator enumerator = getCameraEnumerator();
|
||||
final String[] deviceNames = enumerator.getDeviceNames();
|
||||
final Set<String> deviceNames = ImmutableSet.copyOf(enumerator.getDeviceNames());
|
||||
for (final String deviceName : deviceNames) {
|
||||
if (enumerator.isFrontFacing(deviceName)) {
|
||||
return Optional.fromNullable(of(enumerator, deviceName));
|
||||
return Optional.fromNullable(of(enumerator, deviceName, deviceNames));
|
||||
}
|
||||
}
|
||||
if (deviceNames.length == 0) {
|
||||
if (deviceNames.size() == 0) {
|
||||
return Optional.absent();
|
||||
} else {
|
||||
return Optional.fromNullable(of(enumerator, deviceNames[0]));
|
||||
return Optional.fromNullable(of(enumerator, Iterables.get(deviceNames, 0), deviceNames));
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static CapturerChoice of(CameraEnumerator enumerator, final String deviceName) {
|
||||
private static CapturerChoice of(CameraEnumerator enumerator, final String deviceName, Set<String> availableCameras) {
|
||||
final CameraVideoCapturer capturer = enumerator.createCapturer(deviceName, null);
|
||||
if (capturer == null) {
|
||||
return null;
|
||||
|
@ -431,7 +458,7 @@ public class WebRTCWrapper {
|
|||
Collections.sort(choices, (a, b) -> b.width - a.width);
|
||||
for (final CameraEnumerationAndroid.CaptureFormat captureFormat : choices) {
|
||||
if (captureFormat.width <= CAPTURING_RESOLUTION) {
|
||||
return new CapturerChoice(capturer, captureFormat);
|
||||
return new CapturerChoice(capturer, captureFormat, availableCameras);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
@ -520,10 +547,12 @@ public class WebRTCWrapper {
|
|||
private static class CapturerChoice {
|
||||
private final CameraVideoCapturer cameraVideoCapturer;
|
||||
private final CameraEnumerationAndroid.CaptureFormat captureFormat;
|
||||
private final Set<String> availableCameras;
|
||||
|
||||
CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat) {
|
||||
CapturerChoice(CameraVideoCapturer cameraVideoCapturer, CameraEnumerationAndroid.CaptureFormat captureFormat, Set<String> cameras) {
|
||||
this.cameraVideoCapturer = cameraVideoCapturer;
|
||||
this.captureFormat = captureFormat;
|
||||
this.availableCameras = cameras;
|
||||
}
|
||||
|
||||
int getFrameRate() {
|
||||
|
|
Before Width: | Height: | Size: 657 B After Width: | Height: | Size: 657 B |
Before Width: | Height: | Size: 689 B After Width: | Height: | Size: 689 B |
After Width: | Height: | Size: 717 B |
Before Width: | Height: | Size: 772 B After Width: | Height: | Size: 772 B |
Before Width: | Height: | Size: 773 B After Width: | Height: | Size: 773 B |
Before Width: | Height: | Size: 750 B After Width: | Height: | Size: 750 B |
Before Width: | Height: | Size: 776 B After Width: | Height: | Size: 776 B |
Before Width: | Height: | Size: 779 B After Width: | Height: | Size: 779 B |
Before Width: | Height: | Size: 687 B After Width: | Height: | Size: 687 B |
Before Width: | Height: | Size: 707 B After Width: | Height: | Size: 707 B |
Before Width: | Height: | Size: 514 B After Width: | Height: | Size: 514 B |
Before Width: | Height: | Size: 525 B After Width: | Height: | Size: 525 B |
After Width: | Height: | Size: 472 B |
Before Width: | Height: | Size: 596 B After Width: | Height: | Size: 596 B |
Before Width: | Height: | Size: 617 B After Width: | Height: | Size: 617 B |
Before Width: | Height: | Size: 595 B After Width: | Height: | Size: 595 B |
Before Width: | Height: | Size: 599 B After Width: | Height: | Size: 599 B |
Before Width: | Height: | Size: 610 B After Width: | Height: | Size: 610 B |
Before Width: | Height: | Size: 558 B After Width: | Height: | Size: 558 B |
Before Width: | Height: | Size: 568 B After Width: | Height: | Size: 568 B |
Before Width: | Height: | Size: 739 B After Width: | Height: | Size: 739 B |
Before Width: | Height: | Size: 769 B After Width: | Height: | Size: 769 B |
After Width: | Height: | Size: 915 B |
Before Width: | Height: | Size: 936 B After Width: | Height: | Size: 936 B |
Before Width: | Height: | Size: 926 B After Width: | Height: | Size: 926 B |
Before Width: | Height: | Size: 915 B After Width: | Height: | Size: 915 B |
Before Width: | Height: | Size: 916 B After Width: | Height: | Size: 916 B |
Before Width: | Height: | Size: 935 B After Width: | Height: | Size: 935 B |
Before Width: | Height: | Size: 857 B After Width: | Height: | Size: 857 B |
Before Width: | Height: | Size: 842 B After Width: | Height: | Size: 842 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 1.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 1.4 KiB |
|
@ -117,19 +117,23 @@
|
|||
|
||||
<RelativeLayout
|
||||
android:id="@+id/button_row"
|
||||
android:layout_width="288dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentBottom="true"
|
||||
android:layout_centerHorizontal="true"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="288dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true">
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/reject_call"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_alignParentLeft="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_call_end_white_48dp"
|
||||
android:visibility="gone"
|
||||
|
@ -139,12 +143,30 @@
|
|||
app:maxImageSize="36dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/accept_call"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_call_white_48dp"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/green700"
|
||||
app:elevation="4dp"
|
||||
app:fabCustomSize="72dp"
|
||||
app:maxImageSize="36dp"
|
||||
tools:visibility="visible" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/in_call_action_left"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_margin="12dp"
|
||||
android:layout_toStartOf="@+id/end_call"
|
||||
android:layout_toLeftOf="@+id/end_call"
|
||||
android:visibility="gone"
|
||||
|
@ -171,7 +193,7 @@
|
|||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_margin="16dp"
|
||||
android:layout_margin="12dp"
|
||||
android:layout_toEndOf="@+id/end_call"
|
||||
android:layout_toRightOf="@+id/end_call"
|
||||
android:visibility="gone"
|
||||
|
@ -180,22 +202,19 @@
|
|||
app:fabSize="mini"
|
||||
app:tint="?attr/icon_tint" />
|
||||
|
||||
|
||||
<android.support.design.widget.FloatingActionButton
|
||||
android:id="@+id/accept_call"
|
||||
android:id="@+id/in_call_action_far_right"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_margin="16dp"
|
||||
android:src="@drawable/ic_call_white_48dp"
|
||||
android:layout_margin="12dp"
|
||||
android:layout_toEndOf="@+id/in_call_action_right"
|
||||
android:layout_toRightOf="@+id/in_call_action_right"
|
||||
android:visibility="gone"
|
||||
app:backgroundTint="@color/green700"
|
||||
app:backgroundTint="?color_background_primary"
|
||||
app:elevation="4dp"
|
||||
app:fabCustomSize="72dp"
|
||||
app:maxImageSize="36dp"
|
||||
tools:visibility="visible" />
|
||||
app:fabSize="mini"
|
||||
app:tint="?attr/icon_tint" />
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
|
|
@ -920,6 +920,7 @@
|
|||
<string name="microphone_unavailable">Your microphone is unavailable</string>
|
||||
<string name="only_one_call_at_a_time">You can only have one call at a time.</string>
|
||||
<string name="return_to_ongoing_call">Return to ongoing call</string>
|
||||
<string name="could_not_switch_camera">Could not switch camera</string>
|
||||
<plurals name="view_users">
|
||||
<item quantity="one">View %1$d Participant</item>
|
||||
<item quantity="other">View %1$d Participants</item>
|
||||
|
|