mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-25 03:11:10 +01:00
Add screen share receive support and improve video calling rotation.
This commit is contained in:
committed by
Greyson Parrelli
parent
513e5b45c5
commit
b9b2924939
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -51,10 +51,6 @@ public final class WebRtcControls {
|
||||
this.participantLimit = participantLimit;
|
||||
}
|
||||
|
||||
boolean canRotateControls() {
|
||||
return !isGroupCall();
|
||||
}
|
||||
|
||||
boolean displayErrorControls() {
|
||||
return isError();
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user