Add screen share receive support and improve video calling rotation.

This commit is contained in:
Cody Henthorne
2021-05-25 12:15:07 -04:00
committed by Greyson Parrelli
parent 513e5b45c5
commit b9b2924939
41 changed files with 665 additions and 397 deletions

View File

@@ -11,18 +11,43 @@ import org.webrtc.VideoSink;
import java.util.WeakHashMap;
/**
* Video sink implementation that handles broadcasting a single source video track to
* multiple {@link VideoSink} consumers.
*
* Also has logic to manage rotating frames before forwarding to prevent each renderer
* from having to copy the frame for rotation.
*/
public class BroadcastVideoSink implements VideoSink {
private final EglBase eglBase;
private final WeakHashMap<VideoSink, Boolean> sinks;
private final WeakHashMap<Object, Point> requestingSizes;
private boolean dirtySizes;
private int deviceOrientationDegrees;
private boolean rotateToRightSide;
private boolean forceRotate;
private boolean rotateWithDevice;
public BroadcastVideoSink(@Nullable EglBase eglBase) {
this.eglBase = eglBase;
this.sinks = new WeakHashMap<>();
this.requestingSizes = new WeakHashMap<>();
this.dirtySizes = true;
public BroadcastVideoSink() {
this(null, false, true, 0);
}
/**
* @param eglBase Rendering context
* @param forceRotate Always rotate video frames regardless of frame dimension
* @param rotateWithDevice Rotate video frame to match device orientation
* @param deviceOrientationDegrees Device orientation in degrees
*/
public BroadcastVideoSink(@Nullable EglBase eglBase, boolean forceRotate, boolean rotateWithDevice, int deviceOrientationDegrees) {
this.eglBase = eglBase;
this.sinks = new WeakHashMap<>();
this.requestingSizes = new WeakHashMap<>();
this.dirtySizes = true;
this.deviceOrientationDegrees = deviceOrientationDegrees;
this.rotateToRightSide = false;
this.forceRotate = forceRotate;
this.rotateWithDevice = rotateWithDevice;
}
public @Nullable EglBase getEglBase() {
@@ -37,13 +62,58 @@ public class BroadcastVideoSink implements VideoSink {
sinks.remove(sink);
}
public void setForceRotate(boolean forceRotate) {
this.forceRotate = forceRotate;
}
public void setRotateWithDevice(boolean rotateWithDevice) {
this.rotateWithDevice = rotateWithDevice;
}
/**
* Set the specific rotation desired when not rotating with device.
*
* Really only needed for properly rotating self camera views.
*/
public void setRotateToRightSide(boolean rotateToRightSide) {
this.rotateToRightSide = rotateToRightSide;
}
public void setDeviceOrientationDegrees(int deviceOrientationDegrees) {
this.deviceOrientationDegrees = deviceOrientationDegrees;
}
@Override
public synchronized void onFrame(@NonNull VideoFrame videoFrame) {
if (videoFrame.getRotatedHeight() < videoFrame.getRotatedWidth() || forceRotate) {
int rotation = calculateRotation();
if (rotation > 0) {
rotation += rotateWithDevice ? videoFrame.getRotation() : 0;
videoFrame = new VideoFrame(videoFrame.getBuffer(), rotation % 360, videoFrame.getTimestampNs());
}
}
for (VideoSink sink : sinks.keySet()) {
sink.onFrame(videoFrame);
}
}
private int calculateRotation() {
if (forceRotate && (deviceOrientationDegrees == 0 || deviceOrientationDegrees == 180)) {
return 0;
}
if (rotateWithDevice) {
if (forceRotate) {
return deviceOrientationDegrees;
} else {
return deviceOrientationDegrees != 0 && deviceOrientationDegrees != 180 ? deviceOrientationDegrees : 270;
}
}
return rotateToRightSide ? 90 : 270;
}
void putRequestingSize(@NonNull Object object, @NonNull Point size) {
synchronized (requestingSizes) {
requestingSizes.put(object, size);

View File

@@ -54,6 +54,7 @@ public class CallParticipantView extends ConstraintLayout {
private AppCompatImageView backgroundAvatar;
private AvatarImageView avatar;
private View rendererFrame;
private TextureViewRenderer renderer;
private ImageView pipAvatar;
private ContactPhoto contactPhoto;
@@ -83,6 +84,7 @@ public class CallParticipantView extends ConstraintLayout {
backgroundAvatar = findViewById(R.id.call_participant_background_avatar);
avatar = findViewById(R.id.call_participant_item_avatar);
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
rendererFrame = findViewById(R.id.call_participant_renderer_frame);
renderer = findViewById(R.id.call_participant_renderer);
audioMuted = findViewById(R.id.call_participant_mic_muted);
infoOverlay = findViewById(R.id.call_participant_info_overlay);
@@ -108,6 +110,7 @@ public class CallParticipantView extends ConstraintLayout {
infoMode = participant.getRecipient().isBlocked() || isMissingMediaKeys(participant);
if (infoMode) {
rendererFrame.setVisibility(View.GONE);
renderer.setVisibility(View.GONE);
renderer.attachBroadcastVideoSink(null);
audioMuted.setVisibility(View.GONE);
@@ -130,7 +133,10 @@ public class CallParticipantView extends ConstraintLayout {
} else {
infoOverlay.setVisibility(View.GONE);
renderer.setVisibility(participant.isVideoEnabled() ? View.VISIBLE : View.GONE);
boolean hasContentToRender = participant.isVideoEnabled() || participant.isScreenSharing();
rendererFrame.setVisibility(hasContentToRender ? View.VISIBLE : View.GONE);
renderer.setVisibility(hasContentToRender ? View.VISIBLE : View.GONE);
if (participant.isVideoEnabled()) {
if (participant.getVideoSink().getEglBase() != null) {

View File

@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.ViewUtil;
import org.webrtc.RendererCommon;
import java.util.Collections;
import java.util.List;
@@ -29,9 +30,10 @@ public class CallParticipantsLayout extends FlexboxLayout {
private static final int MULTIPLE_PARTICIPANT_SPACING = ViewUtil.dpToPx(3);
private static final int CORNER_RADIUS = ViewUtil.dpToPx(10);
private List<CallParticipant> callParticipants = Collections.emptyList();
private List<CallParticipant> callParticipants = Collections.emptyList();
private CallParticipant focusedParticipant = null;
private boolean shouldRenderInPip;
private boolean isPortrait;
public CallParticipantsLayout(@NonNull Context context) {
super(context);
@@ -45,10 +47,11 @@ public class CallParticipantsLayout extends FlexboxLayout {
super(context, attrs, defStyleAttr);
}
void update(@NonNull List<CallParticipant> callParticipants, @NonNull CallParticipant focusedParticipant, boolean shouldRenderInPip) {
void update(@NonNull List<CallParticipant> callParticipants, @NonNull CallParticipant focusedParticipant, boolean shouldRenderInPip, boolean isPortrait) {
this.callParticipants = callParticipants;
this.focusedParticipant = focusedParticipant;
this.shouldRenderInPip = shouldRenderInPip;
this.isPortrait = isPortrait;
updateLayout();
}
@@ -104,6 +107,11 @@ public class CallParticipantsLayout extends FlexboxLayout {
callParticipantView.setCallParticipant(participant);
callParticipantView.setRenderInPip(shouldRenderInPip);
if (participant.isScreenSharing()) {
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
} else {
callParticipantView.setScalingType(isPortrait || count < 3 ? RendererCommon.ScalingType.SCALE_ASPECT_FILL : RendererCommon.ScalingType.SCALE_ASPECT_BALANCED);
}
if (count > 1) {
view.setPadding(MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING, MULTIPLE_PARTICIPANT_SPACING);

View File

@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components.webrtc;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.annimon.stream.ComparatorCompat;
import com.annimon.stream.OptionalLong;
@@ -28,16 +27,16 @@ public final class CallParticipantsState {
private static final int SMALL_GROUP_MAX = 6;
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
WebRtcViewModel.GroupCallState.IDLE,
new ParticipantCollection(SMALL_GROUP_MAX),
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(null), false),
null,
WebRtcLocalRenderState.GONE,
false,
false,
false,
OptionalLong.empty());
public static final CallParticipantsState STARTING_STATE = new CallParticipantsState(WebRtcViewModel.State.CALL_DISCONNECTED,
WebRtcViewModel.GroupCallState.IDLE,
new ParticipantCollection(SMALL_GROUP_MAX),
CallParticipant.createLocal(CameraState.UNKNOWN, new BroadcastVideoSink(), false),
CallParticipant.EMPTY,
WebRtcLocalRenderState.GONE,
false,
false,
false,
OptionalLong.empty());
private final WebRtcViewModel.State callState;
private final WebRtcViewModel.GroupCallState groupCallState;
@@ -54,7 +53,7 @@ public final class CallParticipantsState {
@NonNull WebRtcViewModel.GroupCallState groupCallState,
@NonNull ParticipantCollection remoteParticipants,
@NonNull CallParticipant localParticipant,
@Nullable CallParticipant focusedParticipant,
@NonNull CallParticipant focusedParticipant,
@NonNull WebRtcLocalRenderState localRenderState,
boolean isInPipMode,
boolean showVideoForOutgoing,
@@ -105,23 +104,38 @@ public final class CallParticipantsState {
switch (remoteParticipants.size()) {
case 0:
return context.getString(R.string.WebRtcCallView__no_one_else_is_here);
case 1:
case 1: {
if (callState == WebRtcViewModel.State.CALL_PRE_JOIN && groupCallState.isNotIdle()) {
return context.getString(R.string.WebRtcCallView__s_is_in_this_call, remoteParticipants.get(0).getShortRecipientDisplayName(context));
} else {
return remoteParticipants.get(0).getRecipientDisplayName(context);
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
} else {
return remoteParticipants.get(0).getRecipientDisplayName(context);
}
}
case 2:
return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call,
remoteParticipants.get(0).getShortRecipientDisplayName(context),
remoteParticipants.get(1).getShortRecipientDisplayName(context));
default:
int others = remoteParticipants.size() - 2;
return context.getResources().getQuantityString(R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
others,
remoteParticipants.get(0).getShortRecipientDisplayName(context),
remoteParticipants.get(1).getShortRecipientDisplayName(context),
others);
}
case 2: {
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
} else {
return context.getString(R.string.WebRtcCallView__s_and_s_are_in_this_call,
remoteParticipants.get(0).getShortRecipientDisplayName(context),
remoteParticipants.get(1).getShortRecipientDisplayName(context));
}
}
default: {
if (focusedParticipant != CallParticipant.EMPTY && focusedParticipant.isScreenSharing()) {
return context.getString(R.string.WebRtcCallView__s_is_presenting, focusedParticipant.getShortRecipientDisplayName(context));
} else {
int others = remoteParticipants.size() - 2;
return context.getResources().getQuantityString(R.plurals.WebRtcCallView__s_s_and_d_others_are_in_this_call,
others,
remoteParticipants.get(0).getShortRecipientDisplayName(context),
remoteParticipants.get(1).getShortRecipientDisplayName(context),
others);
}
}
}
}
@@ -133,7 +147,7 @@ public final class CallParticipantsState {
return localParticipant;
}
public @Nullable CallParticipant getFocusedParticipant() {
public @NonNull CallParticipant getFocusedParticipant() {
return focusedParticipant;
}
@@ -149,8 +163,16 @@ public final class CallParticipantsState {
return isInPipMode;
}
public boolean isViewingFocusedParticipant() {
return isViewingFocusedParticipant;
}
public boolean needsNewRequestSizes() {
return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize());
if (groupCallState.isNotIdle()) {
return Stream.of(getAllRemoteParticipants()).anyMatch(p -> p.getVideoSink().needsNewRequestingSize());
} else {
return false;
}
}
public @NonNull OptionalLong getRemoteDevicesCount() {
@@ -184,16 +206,11 @@ public final class CallParticipantsState {
oldState.isViewingFocusedParticipant,
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
List<CallParticipant> participantsByLastSpoke = new ArrayList<>(webRtcViewModel.getRemoteParticipants());
Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke())));
CallParticipant focused = participantsByLastSpoke.isEmpty() ? null : participantsByLastSpoke.get(0);
return new CallParticipantsState(webRtcViewModel.getState(),
webRtcViewModel.getGroupState(),
oldState.remoteParticipants.getNext(webRtcViewModel.getRemoteParticipants()),
webRtcViewModel.getLocalParticipant(),
focused,
getFocusedParticipant(webRtcViewModel.getRemoteParticipants()),
localRenderState,
oldState.isInPipMode,
newShowVideoForOutgoing,
@@ -211,13 +228,11 @@ public final class CallParticipantsState {
oldState.isViewingFocusedParticipant,
oldState.getLocalRenderState() == WebRtcLocalRenderState.EXPANDED);
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
return new CallParticipantsState(oldState.callState,
oldState.groupCallState,
oldState.remoteParticipants,
oldState.localParticipant,
focused,
oldState.focusedParticipant,
localRenderState,
isInPip,
oldState.showVideoForOutgoing,
@@ -248,8 +263,6 @@ public final class CallParticipantsState {
}
public static @NonNull CallParticipantsState update(@NonNull CallParticipantsState oldState, @NonNull SelectedPage selectedPage) {
CallParticipant focused = oldState.remoteParticipants.isEmpty() ? null : oldState.remoteParticipants.get(0);
WebRtcLocalRenderState localRenderState = determineLocalRenderMode(oldState.localParticipant,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
@@ -263,7 +276,7 @@ public final class CallParticipantsState {
oldState.groupCallState,
oldState.remoteParticipants,
oldState.localParticipant,
focused,
oldState.focusedParticipant,
localRenderState,
oldState.isInPipMode,
oldState.showVideoForOutgoing,
@@ -304,6 +317,16 @@ public final class CallParticipantsState {
return localRenderState;
}
private static @NonNull CallParticipant getFocusedParticipant(@NonNull List<CallParticipant> participants) {
List<CallParticipant> participantsByLastSpoke = new ArrayList<>(participants);
Collections.sort(participantsByLastSpoke, ComparatorCompat.reversed((p1, p2) -> Long.compare(p1.getLastSpoke(), p2.getLastSpoke())));
return participantsByLastSpoke.isEmpty() ? CallParticipant.EMPTY
: participantsByLastSpoke.stream()
.filter(CallParticipant::isScreenSharing)
.findAny().orElse(participantsByLastSpoke.get(0));
}
public enum SelectedPage {
GRID,
FOCUSED

View File

@@ -0,0 +1,54 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupWindow;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.util.ViewUtil;
import java.util.concurrent.TimeUnit;
/**
* Top screen toast to be shown to the user for 3 seconds.
*
* Currently hard coded to show specific text, but could be easily expanded to be customizable
* if desired. Based on {@link CallParticipantsListUpdatePopupWindow}.
*/
public class CallToastPopupWindow extends PopupWindow {
private static final long DURATION = TimeUnit.SECONDS.toMillis(3);
private final ViewGroup parent;
public static void show(@NonNull ViewGroup viewGroup) {
CallToastPopupWindow toast = new CallToastPopupWindow(viewGroup);
toast.show();
}
private CallToastPopupWindow(@NonNull ViewGroup parent) {
super(LayoutInflater.from(parent.getContext()).inflate(R.layout.call_toast_popup_window, parent, false),
ViewGroup.LayoutParams.MATCH_PARENT,
ViewUtil.dpToPx(94));
this.parent = parent;
setAnimationStyle(R.style.PopupAnimation);
}
public void show() {
showAtLocation(parent, Gravity.TOP | Gravity.START, 0, 0);
measureChild();
update();
getContentView().postDelayed(this::dismiss, DURATION);
}
private void measureChild() {
getContentView().measure(View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED),
View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED));
}
}

View File

@@ -1,24 +0,0 @@
package org.thoughtcrime.securesms.components.webrtc;
import androidx.annotation.NonNull;
import org.webrtc.VideoFrame;
import org.webrtc.VideoSink;
public final class OrientationAwareVideoSink implements VideoSink {
private final VideoSink delegate;
public OrientationAwareVideoSink(@NonNull VideoSink delegate) {
this.delegate = delegate;
}
@Override
public void onFrame(VideoFrame videoFrame) {
if (videoFrame.getRotatedHeight() < videoFrame.getRotatedWidth()) {
delegate.onFrame(new VideoFrame(videoFrame.getBuffer(), 270, videoFrame.getTimestampNs()));
} else {
delegate.onFrame(videoFrame);
}
}
}

View File

@@ -14,29 +14,34 @@ class WebRtcCallParticipantsPage {
private final CallParticipant focusedParticipant;
private final boolean isSpeaker;
private final boolean isRenderInPip;
private final boolean isPortrait;
static WebRtcCallParticipantsPage forMultipleParticipants(@NonNull List<CallParticipant> callParticipants,
@NonNull CallParticipant focusedParticipant,
boolean isRenderInPip)
boolean isRenderInPip,
boolean isPortrait)
{
return new WebRtcCallParticipantsPage(callParticipants, focusedParticipant, false, isRenderInPip);
return new WebRtcCallParticipantsPage(callParticipants, focusedParticipant, false, isRenderInPip, isPortrait);
}
static WebRtcCallParticipantsPage forSingleParticipant(@NonNull CallParticipant singleParticipant,
boolean isRenderInPip)
boolean isRenderInPip,
boolean isPortrait)
{
return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), singleParticipant, true, isRenderInPip);
return new WebRtcCallParticipantsPage(Collections.singletonList(singleParticipant), singleParticipant, true, isRenderInPip, isPortrait);
}
private WebRtcCallParticipantsPage(@NonNull List<CallParticipant> callParticipants,
@NonNull CallParticipant focusedParticipant,
@NonNull CallParticipant focusedParticipant,
boolean isSpeaker,
boolean isRenderInPip)
boolean isRenderInPip,
boolean isPortrait)
{
this.callParticipants = callParticipants;
this.focusedParticipant = focusedParticipant;
this.isSpeaker = isSpeaker;
this.isRenderInPip = isRenderInPip;
this.isPortrait = isPortrait;
}
public @NonNull List<CallParticipant> getCallParticipants() {
@@ -55,19 +60,24 @@ class WebRtcCallParticipantsPage {
return isSpeaker;
}
public boolean isPortrait() {
return isPortrait;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
WebRtcCallParticipantsPage that = (WebRtcCallParticipantsPage) o;
return isSpeaker == that.isSpeaker &&
isRenderInPip == that.isRenderInPip &&
focusedParticipant.equals(that.focusedParticipant) &&
callParticipants.equals(that.callParticipants);
isRenderInPip == that.isRenderInPip &&
focusedParticipant.equals(that.focusedParticipant) &&
callParticipants.equals(that.callParticipants) &&
isPortrait == that.isPortrait;
}
@Override
public int hashCode() {
return Objects.hash(callParticipants, isSpeaker, focusedParticipant, isRenderInPip);
return Objects.hash(callParticipants, isSpeaker, focusedParticipant, isRenderInPip, isPortrait);
}
}

View File

@@ -10,6 +10,8 @@ import androidx.recyclerview.widget.ListAdapter;
import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.webrtc.RendererCommon;
class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipantsPage, WebRtcCallParticipantsPagerAdapter.ViewHolder> {
@@ -84,7 +86,7 @@ class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipa
@Override
void bind(WebRtcCallParticipantsPage page) {
callParticipantsLayout.update(page.getCallParticipants(), page.getFocusedParticipant(), page.isRenderInPip());
callParticipantsLayout.update(page.getCallParticipants(), page.getFocusedParticipant(), page.isRenderInPip(), page.isPortrait());
}
}
@@ -107,8 +109,14 @@ class WebRtcCallParticipantsPagerAdapter extends ListAdapter<WebRtcCallParticipa
@Override
void bind(WebRtcCallParticipantsPage page) {
callParticipantView.setCallParticipant(page.getCallParticipants().get(0));
CallParticipant participant = page.getCallParticipants().get(0);
callParticipantView.setCallParticipant(participant);
callParticipantView.setRenderInPip(page.isRenderInPip());
if (participant.isScreenSharing()) {
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
} else {
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
}
}
}

View File

@@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.webrtc.RendererCommon;
class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant, WebRtcCallParticipantsRecyclerAdapter.ViewHolder> {
@@ -61,6 +62,7 @@ class WebRtcCallParticipantsRecyclerAdapter extends ListAdapter<CallParticipant,
void bind(@NonNull CallParticipant callParticipant) {
callParticipantView.setCallParticipant(callParticipant);
callParticipantView.setRenderInPip(true);
callParticipantView.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL);
}
}

View File

@@ -1,9 +1,5 @@
package org.thoughtcrime.securesms.components.webrtc;
import android.animation.Animator;
import android.animation.AnimatorInflater;
import android.animation.AnimatorSet;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.ColorMatrix;
import android.graphics.ColorMatrixColorFilter;
@@ -12,13 +8,7 @@ import android.util.AttributeSet;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewPropertyAnimator;
import android.view.animation.AlphaAnimation;
import android.view.animation.Animation;
import android.view.animation.AnimationSet;
import android.view.animation.AnimationUtils;
import android.view.animation.ScaleAnimation;
import android.view.animation.TranslateAnimation;
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.TextView;
@@ -43,10 +33,10 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.resource.bitmap.CenterCrop;
import com.google.android.material.button.MaterialButton;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.animation.ResizeAnimation;
import org.thoughtcrime.securesms.components.AccessibleToggleButton;
import org.thoughtcrime.securesms.components.sensors.Orientation;
import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
@@ -67,6 +57,8 @@ import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
public class WebRtcCallView extends FrameLayout {
private static final long TRANSITION_DURATION_MILLIS = 250;
@@ -313,15 +305,15 @@ public class WebRtcCallView extends FrameLayout {
micToggle.setChecked(isMicEnabled, false);
}
public void updateCallParticipants(@NonNull CallParticipantsState state) {
public void updateCallParticipants(@NonNull CallParticipantsState state, boolean isPortrait) {
List<WebRtcCallParticipantsPage> pages = new ArrayList<>(2);
if (!state.getGridParticipants().isEmpty()) {
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.getFocusedParticipant(), state.isInPipMode()));
pages.add(WebRtcCallParticipantsPage.forMultipleParticipants(state.getGridParticipants(), state.getFocusedParticipant(), state.isInPipMode(), isPortrait));
}
if (state.getFocusedParticipant() != null && state.getAllRemoteParticipants().size() > 1) {
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode()));
if (state.getFocusedParticipant() != CallParticipant.EMPTY && state.getAllRemoteParticipants().size() > 1) {
pages.add(WebRtcCallParticipantsPage.forSingleParticipant(state.getFocusedParticipant(), state.isInPipMode(), isPortrait));
}
if ((state.getGroupCallState().isNotIdle() && state.getRemoteDevicesCount().orElse(0) > 0) || state.getGroupCallState().isConnected()) {
@@ -839,6 +831,12 @@ public class WebRtcCallView extends FrameLayout {
return true;
}
public void switchToSpeakerView() {
if (pagerAdapter.getItemCount() > 0) {
callParticipantsPager.setCurrentItem(pagerAdapter.getItemCount() - 1, false);
}
}
public interface ControlsListener {
void onStartCall(boolean isVideoCall);
void onCancelStartCall();

View File

@@ -61,19 +61,15 @@ public class WebRtcCallViewModel extends ViewModel {
private boolean answerWithVideoAvailable = false;
private Runnable elapsedTimeRunnable = this::handleTick;
private boolean canEnterPipMode = false;
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
private boolean callStarting = false;
private List<CallParticipant> previousParticipantsList = Collections.emptyList();
private boolean callStarting = false;
private boolean switchOnFirstScreenShare = true;
private boolean showScreenShareTip = true;
private final WebRtcCallRepository repository = new WebRtcCallRepository(ApplicationDependencies.getApplication());
private WebRtcCallViewModel(@NonNull DeviceOrientationMonitor deviceOrientationMonitor) {
orientation = LiveDataUtil.combineLatest(deviceOrientationMonitor.getOrientation(), webRtcControls, (deviceOrientation, controls) -> {
if (controls.canRotateControls()) {
return deviceOrientation;
} else {
return Orientation.PORTRAIT_BOTTOM_EDGE;
}
});
orientation = deviceOrientationMonitor.getOrientation();
}
public LiveData<Orientation> getOrientation() {
@@ -150,7 +146,16 @@ public class WebRtcCallViewModel extends ViewModel {
SignalStore.tooltips().markGroupCallSpeakerViewSeen();
}
//noinspection ConstantConditions
CallParticipantsState state = participantsState.getValue();
if (state != null &&
showScreenShareTip &&
state.getFocusedParticipant().isScreenSharing() &&
state.isViewingFocusedParticipant() &&
page == CallParticipantsState.SelectedPage.GRID) {
showScreenShareTip = false;
events.setValue(new Event.ShowSwipeToSpeakerHint());
}
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), page));
}
@@ -179,8 +184,16 @@ public class WebRtcCallViewModel extends ViewModel {
microphoneEnabled.setValue(localParticipant.isMicrophoneEnabled());
//noinspection ConstantConditions
participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), webRtcViewModel, enableVideo));
CallParticipantsState state = participantsState.getValue();
if (state != null) {
boolean wasScreenSharing = state.getFocusedParticipant().isScreenSharing();
CallParticipantsState newState = CallParticipantsState.update(state, webRtcViewModel, enableVideo);
participantsState.setValue(newState);
if (switchOnFirstScreenShare && !wasScreenSharing && newState.getFocusedParticipant().isScreenSharing()) {
switchOnFirstScreenShare = false;
events.setValue(new Event.SwitchToSpeaker());
}
}
if (webRtcViewModel.getGroupState().isConnected()) {
if (!containsPlaceholders(previousParticipantsList)) {
@@ -394,6 +407,12 @@ public class WebRtcCallViewModel extends ViewModel {
return identityRecords;
}
}
public static class SwitchToSpeaker extends Event {
}
public static class ShowSwipeToSpeakerHint extends Event {
}
}
public static class SafetyNumberChangeEvent {

View File

@@ -51,10 +51,6 @@ public final class WebRtcControls {
this.participantLimit = participantLimit;
}
boolean canRotateControls() {
return !isGroupCall();
}
boolean displayErrorControls() {
return isError();
}

View File

@@ -10,14 +10,16 @@ import org.thoughtcrime.securesms.util.viewholders.RecipientViewHolder;
public class CallParticipantViewHolder extends RecipientViewHolder<CallParticipantViewState> {
private final ImageView videoMuted;
private final ImageView audioMuted;
private final View videoMuted;
private final View audioMuted;
private final View screenSharing;
public CallParticipantViewHolder(@NonNull View itemView) {
super(itemView, null);
videoMuted = itemView.findViewById(R.id.call_participant_video_muted);
audioMuted = itemView.findViewById(R.id.call_participant_audio_muted);
videoMuted = findViewById(R.id.call_participant_video_muted);
audioMuted = findViewById(R.id.call_participant_audio_muted);
screenSharing = findViewById(R.id.call_participant_screen_sharing);
}
@Override
@@ -26,5 +28,6 @@ public class CallParticipantViewHolder extends RecipientViewHolder<CallParticipa
videoMuted.setVisibility(model.getVideoMutedVisibility());
audioMuted.setVisibility(model.getAudioMutedVisibility());
screenSharing.setVisibility(model.getScreenSharingVisibility());
}
}

View File

@@ -36,6 +36,10 @@ public final class CallParticipantViewState extends RecipientMappingModel<CallPa
return callParticipant.isMicrophoneEnabled() ? View.GONE : View.VISIBLE;
}
public int getScreenSharingVisibility() {
return callParticipant.isScreenSharing() ? View.VISIBLE : View.GONE;
}
@Override
public boolean areItemsTheSame(@NonNull CallParticipantViewState newItem) {
return callParticipant.getCallParticipantId().equals(newItem.callParticipant.getCallParticipantId());