Add screen share receive support and improve video calling rotation.
This commit is contained in:
parent
513e5b45c5
commit
b9b2924939
41 changed files with 665 additions and 397 deletions
|
@ -384,7 +384,7 @@ dependencies {
|
|||
|
||||
implementation 'org.signal:argon2:13.1@aar'
|
||||
|
||||
implementation 'org.signal:ringrtc-android:2.9.6'
|
||||
implementation 'org.signal:ringrtc-android:2.10.1.1'
|
||||
|
||||
implementation "me.leolin:ShortcutBadger:1.1.22"
|
||||
implementation 'se.emilsjolander:stickylistheaders:2.7.0'
|
||||
|
|
|
@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.components.TooltipPopup;
|
|||
import org.thoughtcrime.securesms.components.sensors.DeviceOrientationMonitor;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsListUpdatePopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState;
|
||||
import org.thoughtcrime.securesms.components.webrtc.CallToastPopupWindow;
|
||||
import org.thoughtcrime.securesms.components.webrtc.GroupCallSafetyNumberChangeNotificationUtil;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcAudioOutput;
|
||||
import org.thoughtcrime.securesms.components.webrtc.WebRtcCallView;
|
||||
|
@ -62,11 +63,15 @@ import org.thoughtcrime.securesms.util.EllapsedTimeFormatter;
|
|||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
import org.whispersystems.libsignal.util.Pair;
|
||||
import org.whispersystems.signalservice.api.messages.calls.HangupMessage;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
|
||||
|
||||
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
|
||||
|
||||
private static final String TAG = Log.tag(WebRtcCallActivity.class);
|
||||
|
@ -253,7 +258,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||
viewModel.getWebRtcControls().observe(this, callScreen::setWebRtcControls);
|
||||
viewModel.getEvents().observe(this, this::handleViewModelEvent);
|
||||
viewModel.getCallTime().observe(this, this::handleCallTime);
|
||||
viewModel.getCallParticipantsState().observe(this, callScreen::updateCallParticipants);
|
||||
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(), viewModel.getOrientation(), (s, o) -> new Pair<>(s, o == PORTRAIT_BOTTOM_EDGE))
|
||||
.observe(this, p -> callScreen.updateCallParticipants(p.first(), p.second()));
|
||||
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
|
||||
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);
|
||||
viewModel.getGroupMembers().observe(this, unused -> updateGroupMembersForGroupCall());
|
||||
|
@ -291,6 +297,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
|
|||
} else if (event instanceof WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) {
|
||||
SafetyNumberChangeDialog.showForGroupCall(getSupportFragmentManager(), ((WebRtcCallViewModel.Event.ShowGroupCallSafetyNumberChange) event).getIdentityRecords());
|
||||
return;
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.SwitchToSpeaker) {
|
||||
callScreen.switchToSpeakerView();
|
||||
return;
|
||||
} else if (event instanceof WebRtcCallViewModel.Event.ShowSwipeToSpeakerHint) {
|
||||
CallToastPopupWindow.show(callScreen);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isInPipMode()) {
|
||||
|
|
|
@ -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());
|
||||
|
|
|
@ -1,216 +0,0 @@
|
|||
package org.thoughtcrime.securesms.events;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.whispersystems.libsignal.IdentityKey;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public final class CallParticipant {
|
||||
|
||||
public static final CallParticipant EMPTY = createRemote(new CallParticipantId(Recipient.UNKNOWN), Recipient.UNKNOWN, null, new BroadcastVideoSink(null), false, false, 0, true, 0, DeviceOrdinal.PRIMARY);
|
||||
|
||||
private final @NonNull CallParticipantId callParticipantId;
|
||||
private final @NonNull CameraState cameraState;
|
||||
private final @NonNull Recipient recipient;
|
||||
private final @Nullable IdentityKey identityKey;
|
||||
private final @NonNull BroadcastVideoSink videoSink;
|
||||
private final boolean videoEnabled;
|
||||
private final boolean microphoneEnabled;
|
||||
private final long lastSpoke;
|
||||
private final boolean mediaKeysReceived;
|
||||
private final long addedToCallTime;
|
||||
private final @NonNull DeviceOrdinal deviceOrdinal;
|
||||
|
||||
public static @NonNull CallParticipant createLocal(@NonNull CameraState cameraState,
|
||||
@NonNull BroadcastVideoSink renderer,
|
||||
boolean microphoneEnabled)
|
||||
{
|
||||
return new CallParticipant(new CallParticipantId(Recipient.self()),
|
||||
Recipient.self(),
|
||||
null,
|
||||
renderer,
|
||||
cameraState,
|
||||
cameraState.isEnabled() && cameraState.getCameraCount() > 0,
|
||||
microphoneEnabled,
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
DeviceOrdinal.PRIMARY);
|
||||
}
|
||||
|
||||
public static @NonNull CallParticipant createRemote(@NonNull CallParticipantId callParticipantId,
|
||||
@NonNull Recipient recipient,
|
||||
@Nullable IdentityKey identityKey,
|
||||
@NonNull BroadcastVideoSink renderer,
|
||||
boolean audioEnabled,
|
||||
boolean videoEnabled,
|
||||
long lastSpoke,
|
||||
boolean mediaKeysReceived,
|
||||
long addedToCallTime,
|
||||
@NonNull DeviceOrdinal deviceOrdinal)
|
||||
{
|
||||
return new CallParticipant(callParticipantId, recipient, identityKey, renderer, CameraState.UNKNOWN, videoEnabled, audioEnabled, lastSpoke, mediaKeysReceived, addedToCallTime, deviceOrdinal);
|
||||
}
|
||||
|
||||
private CallParticipant(@NonNull CallParticipantId callParticipantId,
|
||||
@NonNull Recipient recipient,
|
||||
@Nullable IdentityKey identityKey,
|
||||
@NonNull BroadcastVideoSink videoSink,
|
||||
@NonNull CameraState cameraState,
|
||||
boolean videoEnabled,
|
||||
boolean microphoneEnabled,
|
||||
long lastSpoke,
|
||||
boolean mediaKeysReceived,
|
||||
long addedToCallTime,
|
||||
@NonNull DeviceOrdinal deviceOrdinal)
|
||||
{
|
||||
this.callParticipantId = callParticipantId;
|
||||
this.recipient = recipient;
|
||||
this.identityKey = identityKey;
|
||||
this.videoSink = videoSink;
|
||||
this.cameraState = cameraState;
|
||||
this.videoEnabled = videoEnabled;
|
||||
this.microphoneEnabled = microphoneEnabled;
|
||||
this.lastSpoke = lastSpoke;
|
||||
this.mediaKeysReceived = mediaKeysReceived;
|
||||
this.addedToCallTime = addedToCallTime;
|
||||
this.deviceOrdinal = deviceOrdinal;
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant withIdentityKey(@Nullable IdentityKey identityKey) {
|
||||
return new CallParticipant(callParticipantId, recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled, lastSpoke, mediaKeysReceived, addedToCallTime, deviceOrdinal);
|
||||
}
|
||||
|
||||
public @NonNull CallParticipant withVideoEnabled(boolean videoEnabled) {
|
||||
return new CallParticipant(callParticipantId, recipient, identityKey, videoSink, cameraState, videoEnabled, microphoneEnabled, lastSpoke, mediaKeysReceived, addedToCallTime, deviceOrdinal);
|
||||
}
|
||||
|
||||
public @NonNull CallParticipantId getCallParticipantId() {
|
||||
return callParticipantId;
|
||||
}
|
||||
|
||||
public @NonNull Recipient getRecipient() {
|
||||
return recipient;
|
||||
}
|
||||
|
||||
public @NonNull String getRecipientDisplayName(@NonNull Context context) {
|
||||
if (recipient.isSelf() && isPrimary()) {
|
||||
return context.getString(R.string.CallParticipant__you);
|
||||
} else if (recipient.isSelf()) {
|
||||
return context.getString(R.string.CallParticipant__you_on_another_device);
|
||||
} else if (isPrimary()) {
|
||||
return recipient.getDisplayName(context);
|
||||
} else {
|
||||
return context.getString(R.string.CallParticipant__s_on_another_device, recipient.getDisplayName(context));
|
||||
}
|
||||
}
|
||||
|
||||
public @NonNull String getShortRecipientDisplayName(@NonNull Context context) {
|
||||
if (recipient.isSelf() && isPrimary()) {
|
||||
return context.getString(R.string.CallParticipant__you);
|
||||
} else if (recipient.isSelf()) {
|
||||
return context.getString(R.string.CallParticipant__you_on_another_device);
|
||||
} else if (isPrimary()) {
|
||||
return recipient.getShortDisplayName(context);
|
||||
} else {
|
||||
return context.getString(R.string.CallParticipant__s_on_another_device, recipient.getShortDisplayName(context));
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable IdentityKey getIdentityKey() {
|
||||
return identityKey;
|
||||
}
|
||||
|
||||
public @NonNull BroadcastVideoSink getVideoSink() {
|
||||
return videoSink;
|
||||
}
|
||||
|
||||
public @NonNull CameraState getCameraState() {
|
||||
return cameraState;
|
||||
}
|
||||
|
||||
public boolean isVideoEnabled() {
|
||||
return videoEnabled;
|
||||
}
|
||||
|
||||
public boolean isMicrophoneEnabled() {
|
||||
return microphoneEnabled;
|
||||
}
|
||||
|
||||
public @NonNull CameraState.Direction getCameraDirection() {
|
||||
if (cameraState.getActiveDirection() == CameraState.Direction.BACK) {
|
||||
return cameraState.getActiveDirection();
|
||||
}
|
||||
return CameraState.Direction.FRONT;
|
||||
}
|
||||
|
||||
public boolean isMoreThanOneCameraAvailable() {
|
||||
return cameraState.getCameraCount() > 1;
|
||||
}
|
||||
|
||||
public long getLastSpoke() {
|
||||
return lastSpoke;
|
||||
}
|
||||
|
||||
public boolean isMediaKeysReceived() {
|
||||
return mediaKeysReceived;
|
||||
}
|
||||
|
||||
public long getAddedToCallTime() {
|
||||
return addedToCallTime;
|
||||
}
|
||||
|
||||
public boolean isPrimary() {
|
||||
return deviceOrdinal == DeviceOrdinal.PRIMARY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
CallParticipant that = (CallParticipant) o;
|
||||
return callParticipantId.equals(that.callParticipantId) &&
|
||||
videoEnabled == that.videoEnabled &&
|
||||
microphoneEnabled == that.microphoneEnabled &&
|
||||
lastSpoke == that.lastSpoke &&
|
||||
mediaKeysReceived == that.mediaKeysReceived &&
|
||||
addedToCallTime == that.addedToCallTime &&
|
||||
cameraState.equals(that.cameraState) &&
|
||||
recipient.equals(that.recipient) &&
|
||||
Objects.equals(identityKey, that.identityKey) &&
|
||||
Objects.equals(videoSink, that.videoSink);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(callParticipantId, cameraState, recipient, identityKey, videoSink, videoEnabled, microphoneEnabled, lastSpoke, mediaKeysReceived, addedToCallTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull String toString() {
|
||||
return "CallParticipant{" +
|
||||
"cameraState=" + cameraState +
|
||||
", recipient=" + recipient.getId() +
|
||||
", identityKey=" + (identityKey == null ? "absent" : "present") +
|
||||
", videoSink=" + (videoSink.getEglBase() == null ? "not initialized" : "initialized") +
|
||||
", videoEnabled=" + videoEnabled +
|
||||
", microphoneEnabled=" + microphoneEnabled +
|
||||
", lastSpoke=" + lastSpoke +
|
||||
", mediaKeysReceived=" + mediaKeysReceived +
|
||||
", addedToCallTime=" + addedToCallTime +
|
||||
'}';
|
||||
}
|
||||
|
||||
public enum DeviceOrdinal {
|
||||
PRIMARY,
|
||||
SECONDARY
|
||||
}
|
||||
}
|
|
@ -0,0 +1,122 @@
|
|||
package org.thoughtcrime.securesms.events
|
||||
|
||||
import android.content.Context
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState
|
||||
import org.whispersystems.libsignal.IdentityKey
|
||||
|
||||
data class CallParticipant constructor(
|
||||
val callParticipantId: CallParticipantId = CallParticipantId(Recipient.UNKNOWN),
|
||||
val recipient: Recipient = Recipient.UNKNOWN,
|
||||
val identityKey: IdentityKey? = null,
|
||||
val videoSink: BroadcastVideoSink = BroadcastVideoSink(),
|
||||
val cameraState: CameraState = CameraState.UNKNOWN,
|
||||
val isVideoEnabled: Boolean = false,
|
||||
val isMicrophoneEnabled: Boolean = false,
|
||||
val lastSpoke: Long = 0,
|
||||
val isMediaKeysReceived: Boolean = true,
|
||||
val addedToCallTime: Long = 0,
|
||||
val isScreenSharing: Boolean = false,
|
||||
private val deviceOrdinal: DeviceOrdinal = DeviceOrdinal.PRIMARY
|
||||
) {
|
||||
val cameraDirection: CameraState.Direction
|
||||
get() = if (cameraState.activeDirection == CameraState.Direction.BACK) cameraState.activeDirection else CameraState.Direction.FRONT
|
||||
|
||||
val isMoreThanOneCameraAvailable: Boolean
|
||||
get() = cameraState.cameraCount > 1
|
||||
|
||||
val isPrimary: Boolean
|
||||
get() = deviceOrdinal == DeviceOrdinal.PRIMARY
|
||||
|
||||
fun getRecipientDisplayName(context: Context): String {
|
||||
return if (recipient.isSelf && isPrimary) {
|
||||
context.getString(R.string.CallParticipant__you)
|
||||
} else if (recipient.isSelf) {
|
||||
context.getString(R.string.CallParticipant__you_on_another_device)
|
||||
} else if (isPrimary) {
|
||||
recipient.getDisplayName(context)
|
||||
} else {
|
||||
context.getString(R.string.CallParticipant__s_on_another_device, recipient.getDisplayName(context))
|
||||
}
|
||||
}
|
||||
|
||||
fun getShortRecipientDisplayName(context: Context): String {
|
||||
return if (recipient.isSelf && isPrimary) {
|
||||
context.getString(R.string.CallParticipant__you)
|
||||
} else if (recipient.isSelf) {
|
||||
context.getString(R.string.CallParticipant__you_on_another_device)
|
||||
} else if (isPrimary) {
|
||||
recipient.getShortDisplayName(context)
|
||||
} else {
|
||||
context.getString(R.string.CallParticipant__s_on_another_device, recipient.getShortDisplayName(context))
|
||||
}
|
||||
}
|
||||
|
||||
fun withIdentityKey(identityKey: IdentityKey?): CallParticipant {
|
||||
return copy(identityKey = identityKey)
|
||||
}
|
||||
|
||||
fun withVideoEnabled(videoEnabled: Boolean): CallParticipant {
|
||||
return copy(isVideoEnabled = videoEnabled)
|
||||
}
|
||||
|
||||
fun withScreenSharingEnabled(enable: Boolean): CallParticipant {
|
||||
return copy(isScreenSharing = enable)
|
||||
}
|
||||
|
||||
enum class DeviceOrdinal {
|
||||
PRIMARY, SECONDARY
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmField
|
||||
val EMPTY: CallParticipant = CallParticipant()
|
||||
|
||||
@JvmStatic
|
||||
fun createLocal(
|
||||
cameraState: CameraState,
|
||||
renderer: BroadcastVideoSink,
|
||||
microphoneEnabled: Boolean
|
||||
): CallParticipant {
|
||||
return CallParticipant(
|
||||
callParticipantId = CallParticipantId(Recipient.self()),
|
||||
recipient = Recipient.self(),
|
||||
videoSink = renderer,
|
||||
cameraState = cameraState,
|
||||
isVideoEnabled = cameraState.isEnabled && cameraState.cameraCount > 0,
|
||||
isMicrophoneEnabled = microphoneEnabled
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun createRemote(
|
||||
callParticipantId: CallParticipantId,
|
||||
recipient: Recipient,
|
||||
identityKey: IdentityKey?,
|
||||
renderer: BroadcastVideoSink,
|
||||
audioEnabled: Boolean,
|
||||
videoEnabled: Boolean,
|
||||
lastSpoke: Long,
|
||||
mediaKeysReceived: Boolean,
|
||||
addedToCallTime: Long,
|
||||
isScreenSharing: Boolean,
|
||||
deviceOrdinal: DeviceOrdinal
|
||||
): CallParticipant {
|
||||
return CallParticipant(
|
||||
callParticipantId = callParticipantId,
|
||||
recipient = recipient,
|
||||
identityKey = identityKey,
|
||||
videoSink = renderer,
|
||||
isVideoEnabled = videoEnabled,
|
||||
isMicrophoneEnabled = audioEnabled,
|
||||
lastSpoke = lastSpoke,
|
||||
isMediaKeysReceived = mediaKeysReceived,
|
||||
addedToCallTime = addedToCallTime,
|
||||
isScreenSharing = isScreenSharing,
|
||||
deviceOrdinal = deviceOrdinal
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -119,7 +119,7 @@ public class WebRtcViewModel {
|
|||
this.participantLimit = state.getCallInfoState().getParticipantLimit();
|
||||
this.localParticipant = CallParticipant.createLocal(state.getLocalDeviceState().getCameraState(),
|
||||
state.getVideoState().getLocalSink() != null ? state.getVideoState().getLocalSink()
|
||||
: new BroadcastVideoSink(null),
|
||||
: new BroadcastVideoSink(),
|
||||
state.getLocalDeviceState().isMicrophoneEnabled());
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import org.webrtc.CameraVideoCapturer;
|
|||
import org.webrtc.CapturerObserver;
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.SurfaceTextureHelper;
|
||||
import org.webrtc.VideoFrame;
|
||||
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
@ -81,7 +82,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
|
|||
if (capturer != null) {
|
||||
capturer.initialize(SurfaceTextureHelper.create("WebRTC-SurfaceTextureHelper", eglBase.getEglBaseContext()),
|
||||
context,
|
||||
observer);
|
||||
new CameraCapturerWrapper(observer));
|
||||
capturer.setOrientation(orientation);
|
||||
isInitialized = true;
|
||||
}
|
||||
|
@ -297,4 +298,30 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa
|
|||
return new Camera2Capturer(context, deviceName, eventsHandler, new FilteredCamera2Enumerator(context));
|
||||
}
|
||||
}
|
||||
|
||||
private class CameraCapturerWrapper implements CapturerObserver {
|
||||
private final CapturerObserver observer;
|
||||
|
||||
public CameraCapturerWrapper(@NonNull CapturerObserver observer) {
|
||||
this.observer = observer;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCapturerStarted(boolean success) {
|
||||
observer.onCapturerStarted(success);
|
||||
if (success) {
|
||||
cameraEventListener.onFullyInitialized();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCapturerStopped() {
|
||||
observer.onCapturerStopped();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFrameCaptured(VideoFrame videoFrame) {
|
||||
observer.onFrameCaptured(videoFrame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,5 +3,6 @@ package org.thoughtcrime.securesms.ringrtc;
|
|||
import androidx.annotation.NonNull;
|
||||
|
||||
public interface CameraEventListener {
|
||||
void onFullyInitialized();
|
||||
void onCameraSwitchCompleted(@NonNull CameraState newCameraState);
|
||||
}
|
||||
|
|
|
@ -95,6 +95,21 @@ public class ActiveCallActionProcessorDelegate extends WebRtcActionProcessor {
|
|||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleScreenSharingEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer();
|
||||
|
||||
Log.i(tag, "handleScreenSharingEnable(): call_id: " + activePeer.getCallId() + " enable: " + enable);
|
||||
|
||||
CallParticipant oldParticipant = Objects.requireNonNull(currentState.getCallInfoState().getRemoteCallParticipant(activePeer.getRecipient()));
|
||||
CallParticipant newParticipant = oldParticipant.withScreenSharingEnabled(enable);
|
||||
|
||||
return currentState.builder()
|
||||
.changeCallInfoState()
|
||||
.putParticipant(activePeer.getRecipient(), newParticipant)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
|
||||
RemotePeer remotePeer = currentState.getCallInfoState().getActivePeer();
|
||||
|
|
|
@ -34,6 +34,7 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
|
|||
@NonNull OfferMessage.Type offerType)
|
||||
{
|
||||
remotePeer.setCallStartTimestamp(System.currentTimeMillis());
|
||||
|
||||
currentState = currentState.builder()
|
||||
.actionProcessor(new OutgoingCallActionProcessor(webRtcInteractor))
|
||||
.changeCallInfoState()
|
||||
|
@ -41,17 +42,20 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
|
|||
.callState(WebRtcViewModel.State.CALL_OUTGOING)
|
||||
.putRemotePeer(remotePeer)
|
||||
.putParticipant(remotePeer.getRecipient(),
|
||||
CallParticipant.createRemote(
|
||||
new CallParticipantId(remotePeer.getRecipient()),
|
||||
remotePeer.getRecipient(),
|
||||
null,
|
||||
new BroadcastVideoSink(currentState.getVideoState().getEglBase()),
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
CallParticipant.DeviceOrdinal.PRIMARY
|
||||
CallParticipant.createRemote(new CallParticipantId(remotePeer.getRecipient()),
|
||||
remotePeer.getRecipient(),
|
||||
null,
|
||||
new BroadcastVideoSink(currentState.getVideoState().getEglBase(),
|
||||
false,
|
||||
true,
|
||||
currentState.getLocalDeviceState().getOrientation().getDegrees()),
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
CallParticipant.DeviceOrdinal.PRIMARY
|
||||
))
|
||||
.build();
|
||||
|
||||
|
@ -85,17 +89,20 @@ public class BeginCallActionProcessorDelegate extends WebRtcActionProcessor {
|
|||
.activePeer(remotePeer)
|
||||
.callState(WebRtcViewModel.State.CALL_INCOMING)
|
||||
.putParticipant(remotePeer.getRecipient(),
|
||||
CallParticipant.createRemote(
|
||||
new CallParticipantId(remotePeer.getRecipient()),
|
||||
remotePeer.getRecipient(),
|
||||
null,
|
||||
new BroadcastVideoSink(currentState.getVideoState().getEglBase()),
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
CallParticipant.DeviceOrdinal.PRIMARY
|
||||
CallParticipant.createRemote(new CallParticipantId(remotePeer.getRecipient()),
|
||||
remotePeer.getRecipient(),
|
||||
null,
|
||||
new BroadcastVideoSink(currentState.getVideoState().getEglBase(),
|
||||
false,
|
||||
true,
|
||||
currentState.getLocalDeviceState().getOrientation().getDegrees()),
|
||||
true,
|
||||
false,
|
||||
0,
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
CallParticipant.DeviceOrdinal.PRIMARY
|
||||
))
|
||||
.build();
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor {
|
|||
|
||||
try {
|
||||
webRtcInteractor.getCallManager().setVideoEnable(enable);
|
||||
} catch (CallException e) {
|
||||
} catch (CallException e) {
|
||||
return callFailure(currentState, "setVideoEnable() failed: ", e);
|
||||
}
|
||||
|
||||
|
@ -77,10 +77,15 @@ public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
protected @NonNull WebRtcServiceState handleRemoteVideoEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
return activeCallDelegate.handleRemoteVideoEnable(currentState, enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleScreenSharingEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
return activeCallDelegate.handleScreenSharingEnable(currentState, enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleSendIceCandidates(@NonNull WebRtcServiceState currentState,
|
||||
@NonNull WebRtcData.CallMetadata callMetadata,
|
||||
|
|
|
@ -5,6 +5,8 @@ import android.media.AudioManager;
|
|||
import androidx.annotation.NonNull;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
|
@ -109,6 +111,11 @@ public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor {
|
|||
public @NonNull WebRtcServiceState handleCameraSwitchCompleted(@NonNull WebRtcServiceState currentState, @NonNull CameraState newCameraState) {
|
||||
Log.i(tag, "handleCameraSwitchCompleted():");
|
||||
|
||||
BroadcastVideoSink localSink = currentState.getVideoState().getLocalSink();
|
||||
if (localSink != null) {
|
||||
localSink.setRotateToRightSide(newCameraState.getActiveDirection() == CameraState.Direction.BACK);
|
||||
}
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.cameraState(newCameraState)
|
||||
|
|
|
@ -93,10 +93,13 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
VideoTrack videoTrack = device.getVideoTrack();
|
||||
if (videoTrack != null) {
|
||||
videoSink = (callParticipant != null && callParticipant.getVideoSink().getEglBase() != null) ? callParticipant.getVideoSink()
|
||||
: new BroadcastVideoSink(currentState.getVideoState().requireEglBase());
|
||||
: new BroadcastVideoSink(currentState.getVideoState().requireEglBase(),
|
||||
true,
|
||||
true,
|
||||
currentState.getLocalDeviceState().getOrientation().getDegrees());
|
||||
videoTrack.addSink(videoSink);
|
||||
} else {
|
||||
videoSink = new BroadcastVideoSink(null);
|
||||
videoSink = new BroadcastVideoSink();
|
||||
}
|
||||
|
||||
builder.putParticipant(callParticipantId,
|
||||
|
@ -109,6 +112,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
device.getSpeakerTime(),
|
||||
device.getMediaKeysReceived(),
|
||||
device.getAddedTime(),
|
||||
Boolean.TRUE.equals(device.getPresenting()),
|
||||
seen.contains(recipient) ? CallParticipant.DeviceOrdinal.SECONDARY
|
||||
: CallParticipant.DeviceOrdinal.PRIMARY));
|
||||
|
||||
|
@ -318,11 +322,6 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor {
|
|||
return terminateGroupCall(currentState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleOrientationChanged(@NonNull WebRtcServiceState currentState, int orientationDegrees) {
|
||||
return currentState;
|
||||
}
|
||||
|
||||
public synchronized @NonNull WebRtcServiceState terminateGroupCall(@NonNull WebRtcServiceState currentState) {
|
||||
return terminateGroupCall(currentState, true);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import com.annimon.stream.Stream;
|
|||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.signal.ringrtc.PeekInfo;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
|
@ -123,7 +122,17 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
|
|||
.clearParticipantMap();
|
||||
|
||||
for (Recipient recipient : callParticipants) {
|
||||
builder.putParticipant(recipient, CallParticipant.createRemote(new CallParticipantId(recipient), recipient, null, new BroadcastVideoSink(null), true, true, 0, false, 0, CallParticipant.DeviceOrdinal.PRIMARY));
|
||||
builder.putParticipant(recipient, CallParticipant.createRemote(new CallParticipantId(recipient),
|
||||
recipient,
|
||||
null,
|
||||
new BroadcastVideoSink(),
|
||||
true,
|
||||
true,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
false,
|
||||
CallParticipant.DeviceOrdinal.PRIMARY));
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
|
|
|
@ -10,7 +10,6 @@ import org.signal.core.util.logging.Log;
|
|||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.components.webrtc.OrientationAwareVideoSink;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
|
@ -24,7 +23,6 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
|
|||
import org.thoughtcrime.securesms.service.webrtc.state.VideoState;
|
||||
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
|
||||
import org.thoughtcrime.securesms.util.NetworkUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
|
||||
import org.webrtc.PeerConnection;
|
||||
import org.whispersystems.signalservice.api.messages.calls.AnswerMessage;
|
||||
|
@ -89,8 +87,8 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
|||
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),
|
||||
context,
|
||||
videoState.requireEglBase(),
|
||||
new OrientationAwareVideoSink(videoState.requireLocalSink()),
|
||||
new OrientationAwareVideoSink(callParticipant.getVideoSink()),
|
||||
videoState.requireLocalSink(),
|
||||
callParticipant.getVideoSink(),
|
||||
videoState.requireCamera(),
|
||||
iceServers,
|
||||
hideIp,
|
||||
|
@ -198,6 +196,11 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
|
|||
return activeCallDelegate.handleRemoteVideoEnable(currentState, enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleScreenSharingEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
return activeCallDelegate.handleScreenSharingEnable(currentState, enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleReceivedOfferWhileActive(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) {
|
||||
return activeCallDelegate.handleReceivedOfferWhileActive(currentState, remotePeer);
|
||||
|
|
|
@ -10,7 +10,6 @@ import org.signal.core.util.logging.Log;
|
|||
import org.signal.ringrtc.CallException;
|
||||
import org.signal.ringrtc.CallId;
|
||||
import org.signal.ringrtc.CallManager;
|
||||
import org.thoughtcrime.securesms.components.webrtc.OrientationAwareVideoSink;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
|
@ -120,8 +119,8 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
|||
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),
|
||||
context,
|
||||
videoState.requireEglBase(),
|
||||
new OrientationAwareVideoSink(videoState.requireLocalSink()),
|
||||
new OrientationAwareVideoSink(callParticipant.getVideoSink()),
|
||||
videoState.requireLocalSink(),
|
||||
callParticipant.getVideoSink(),
|
||||
videoState.requireCamera(),
|
||||
iceServers,
|
||||
isAlwaysTurn,
|
||||
|
@ -198,6 +197,11 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
|
|||
return activeCallDelegate.handleRemoteVideoEnable(currentState, enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleScreenSharingEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
return activeCallDelegate.handleScreenSharingEnable(currentState, enable);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) {
|
||||
return activeCallDelegate.handleLocalHangup(currentState);
|
||||
|
|
|
@ -394,6 +394,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
|
|||
return p.handleRemoteVideoEnable(s, true);
|
||||
case REMOTE_VIDEO_DISABLE:
|
||||
return p.handleRemoteVideoEnable(s, false);
|
||||
case REMOTE_SHARING_SCREEN_ENABLE:
|
||||
return p.handleScreenSharingEnable(s, true);
|
||||
case REMOTE_SHARING_SCREEN_DISABLE:
|
||||
return p.handleScreenSharingEnable(s, false);
|
||||
case ENDED_REMOTE_HANGUP:
|
||||
case ENDED_REMOTE_HANGUP_NEED_PERMISSION:
|
||||
case ENDED_REMOTE_HANGUP_ACCEPTED:
|
||||
|
@ -641,6 +645,11 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
|
|||
process((s, p) -> p.handleGroupCallEnded(s, groupCall.hashCode(), groupCallEndReason));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFullyInitialized() {
|
||||
process((s, p) -> p.handleOrientationChanged(s, s.getLocalDeviceState().getOrientation().getDegrees()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCameraSwitchCompleted(@NonNull final CameraState newCameraState) {
|
||||
process((s, p) -> p.handleCameraSwitchCompleted(s, newCameraState));
|
||||
|
|
|
@ -12,6 +12,7 @@ import org.signal.ringrtc.CallId;
|
|||
import org.signal.ringrtc.CallManager;
|
||||
import org.signal.ringrtc.GroupCall;
|
||||
import org.thoughtcrime.securesms.components.sensors.Orientation;
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.crypto.IdentityKeyUtil;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.events.CallParticipant;
|
||||
|
@ -276,6 +277,11 @@ public abstract class WebRtcActionProcessor {
|
|||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleScreenSharingEnable(@NonNull WebRtcServiceState currentState, boolean enable) {
|
||||
Log.i(tag, "handleScreenSharingEnable not processed");
|
||||
return currentState;
|
||||
}
|
||||
|
||||
protected @NonNull WebRtcServiceState handleReceivedHangup(@NonNull WebRtcServiceState currentState,
|
||||
@NonNull CallMetadata callMetadata,
|
||||
@NonNull HangupMetadata hangupMetadata)
|
||||
|
@ -454,6 +460,15 @@ public abstract class WebRtcActionProcessor {
|
|||
camera.setOrientation(orientationDegrees);
|
||||
}
|
||||
|
||||
BroadcastVideoSink sink = currentState.getVideoState().getLocalSink();
|
||||
if (sink != null) {
|
||||
sink.setDeviceOrientationDegrees(orientationDegrees);
|
||||
}
|
||||
|
||||
for (CallParticipant callParticipant : currentState.getCallInfoState().getRemoteCallParticipants()) {
|
||||
callParticipant.getVideoSink().setDeviceOrientationDegrees(orientationDegrees);
|
||||
}
|
||||
|
||||
return currentState.builder()
|
||||
.changeLocalDeviceState()
|
||||
.setOrientation(Orientation.fromDegrees(orientationDegrees))
|
||||
|
|
|
@ -6,7 +6,6 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink;
|
||||
import org.thoughtcrime.securesms.components.webrtc.OrientationAwareVideoSink;
|
||||
import org.thoughtcrime.securesms.ringrtc.Camera;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
|
||||
import org.thoughtcrime.securesms.ringrtc.CameraState;
|
||||
|
@ -33,7 +32,10 @@ public final class WebRtcVideoUtil {
|
|||
|
||||
ThreadUtil.runOnMainSync(() -> {
|
||||
EglBase eglBase = EglBase.create();
|
||||
BroadcastVideoSink localSink = new BroadcastVideoSink(eglBase);
|
||||
BroadcastVideoSink localSink = new BroadcastVideoSink(eglBase,
|
||||
true,
|
||||
false,
|
||||
currentState.getLocalDeviceState().getOrientation().getDegrees());
|
||||
Camera camera = new Camera(context, cameraEventListener, eglBase, CameraState.Direction.FRONT);
|
||||
|
||||
camera.setOrientation(currentState.getLocalDeviceState().getOrientation().getDegrees());
|
||||
|
@ -104,7 +106,7 @@ public final class WebRtcVideoUtil {
|
|||
|
||||
public static @NonNull WebRtcServiceState initializeVanityCamera(@NonNull WebRtcServiceState currentState) {
|
||||
Camera camera = currentState.getVideoState().requireCamera();
|
||||
VideoSink sink = new OrientationAwareVideoSink(currentState.getVideoState().requireLocalSink());
|
||||
VideoSink sink = currentState.getVideoState().requireLocalSink();
|
||||
|
||||
if (camera.hasCapturer()) {
|
||||
camera.initCapturer(new CapturerObserver() {
|
||||
|
|
9
app/src/main/res/drawable/ic_share_screen_20.xml
Normal file
9
app/src/main/res/drawable/ic_share_screen_20.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="20dp"
|
||||
android:height="20dp"
|
||||
android:viewportWidth="20"
|
||||
android:viewportHeight="20">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M17,3L3,3A2,2 0,0 0,1 5L1,15a2,2 0,0 0,2 2L17,17a2,2 0,0 0,2 -2L19,5A2,2 0,0 0,17 3ZM12.47,10.53 L11.37,9.43 10.75,8.5L10.75,14L9.25,14L9.25,8.5l-0.62,0.93 -1.1,1.1L6.47,9.47 10,5.94l3.53,3.53Z"/>
|
||||
</vector>
|
|
@ -39,15 +39,25 @@
|
|||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:srcCompat="@tools:sample/avatars" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
|
||||
android:id="@+id/call_participant_renderer"
|
||||
<FrameLayout
|
||||
android:id="@+id/call_participant_renderer_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/core_grey_80"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<org.thoughtcrime.securesms.components.webrtc.TextureViewRenderer
|
||||
android:id="@+id/call_participant_renderer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/call_participant_mic_muted"
|
||||
|
|
|
@ -47,4 +47,12 @@
|
|||
app:srcCompat="@drawable/ic_mic_off_solid_18"
|
||||
app:tint="@color/core_white"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/call_participant_screen_sharing"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="12dp"
|
||||
app:srcCompat="@drawable/ic_share_screen_20"
|
||||
app:tint="@color/core_white"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
|
35
app/src/main/res/layout/call_toast_popup_window.xml
Normal file
35
app/src/main/res/layout/call_toast_popup_window.xml
Normal file
|
@ -0,0 +1,35 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="94dp">
|
||||
|
||||
<FrameLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center_horizontal"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_marginTop="30dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:background="@drawable/call_participant_update_window_background"
|
||||
android:minHeight="44dp"
|
||||
android:padding="12dp">
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:drawablePadding="8dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/CallToastPopupWindow__swipe_to_view_screen_share"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body2"
|
||||
app:drawableStartCompat="@drawable/ic_arrow_down"
|
||||
app:drawableTint="@color/signal_text_primary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/avatar"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
</FrameLayout>
|
||||
</FrameLayout>
|
|
@ -32,17 +32,20 @@
|
|||
android:layout_height="match_parent"
|
||||
android:layout="@layout/group_call_call_full" />
|
||||
|
||||
<View
|
||||
android:id="@+id/call_screen_header_gradient"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp"
|
||||
android:background="@drawable/webrtc_call_screen_header_gradient" />
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
android:id="@+id/call_screen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<View
|
||||
android:id="@+id/call_screen_header_gradient"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="160dp"
|
||||
android:background="@drawable/webrtc_call_screen_header_gradient"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<androidx.constraintlayout.widget.Guideline
|
||||
android:id="@+id/call_screen_status_bar_guideline"
|
||||
android:layout_width="wrap_content"
|
||||
|
@ -93,7 +96,7 @@
|
|||
android:id="@+id/call_screen_pip_area"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintBottom_toTopOf="@+id/call_screen_navigation_bar_guideline"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
|
|
@ -1407,6 +1407,7 @@
|
|||
<string name="WebRtcCallView__no_one_else_is_here">No one else is here</string>
|
||||
<string name="WebRtcCallView__s_is_in_this_call">%1$s is in this call</string>
|
||||
<string name="WebRtcCallView__s_and_s_are_in_this_call">%1$s and %2$s are in this call</string>
|
||||
<string name="WebRtcCallView__s_is_presenting">%1$s is presenting</string>
|
||||
|
||||
<plurals name="WebRtcCallView__s_s_and_d_others_are_in_this_call">
|
||||
<item quantity="one">%1$s, %2$s, and %3$d other are in this call</item>
|
||||
|
@ -1427,6 +1428,9 @@
|
|||
<string name="CallParticipantView__cant_receive_audio_and_video_from_s">Can\'t receive audio and video from %1$s</string>
|
||||
<string name="CallParticipantView__this_may_be_Because_they_have_not_verified_your_safety_number_change">This may be because they have not verified your safety number change, there\'s a problem with their device, or they have blocked you.</string>
|
||||
|
||||
<!-- CallToastPopupWindow -->
|
||||
<string name="CallToastPopupWindow__swipe_to_view_screen_share">Swipe to view screen share</string>
|
||||
|
||||
<!-- ProxyBottomSheetFragment -->
|
||||
<string name="ProxyBottomSheetFragment_proxy_server">Proxy server</string>
|
||||
<string name="ProxyBottomSheetFragment_proxy_address">Proxy address</string>
|
||||
|
|
|
@ -178,7 +178,7 @@ public class CallParticipantListUpdateTest {
|
|||
private static CallParticipant createParticipant(long recipientId, long deMuxId, @NonNull CallParticipant.DeviceOrdinal deviceOrdinal) {
|
||||
Recipient recipient = new Recipient(RecipientId.from(recipientId), mock(RecipientDetails.class), true);
|
||||
|
||||
return CallParticipant.createRemote(new CallParticipantId(deMuxId, recipient.getId()), recipient, null, new BroadcastVideoSink(null), false, false, -1, false, 0, deviceOrdinal);
|
||||
return CallParticipant.createRemote(new CallParticipantId(deMuxId, recipient.getId()), recipient, null, new BroadcastVideoSink(), false, false, -1, false, 0, false, deviceOrdinal);
|
||||
}
|
||||
|
||||
}
|
|
@ -239,12 +239,13 @@ public class ParticipantCollectionTest {
|
|||
new CallParticipantId(serializedId, RecipientId.from(serializedId)),
|
||||
Recipient.UNKNOWN,
|
||||
null,
|
||||
new BroadcastVideoSink(null),
|
||||
new BroadcastVideoSink(),
|
||||
false,
|
||||
false,
|
||||
lastSpoke,
|
||||
false,
|
||||
added,
|
||||
false,
|
||||
CallParticipant.DeviceOrdinal.PRIMARY);
|
||||
}
|
||||
}
|
|
@ -495,8 +495,8 @@ dependencyVerification {
|
|||
['org.signal:argon2:13.1',
|
||||
'0f686ccff0d4842bfcc74d92e8dc780a5f159b9376e37a1189fabbcdac458bef'],
|
||||
|
||||
['org.signal:ringrtc-android:2.9.6',
|
||||
'daa06a2a31fb2c6001319ba30d3f412d8ed8de5eae8b49214a73c507f2d0eee9'],
|
||||
['org.signal:ringrtc-android:2.10.1.1',
|
||||
'a246d87001d485a76c88b6ba83451773e96ef7f19f1949a21e19de6d80f67b2d'],
|
||||
|
||||
['org.signal:zkgroup-android:0.7.0',
|
||||
'52b172565bd01526e93ebf1796b834bdc449d4fe3422c1b827e49cb8d4f13fbd'],
|
||||
|
|
Loading…
Add table
Reference in a new issue