Merge pull request #2995 from nextcloud/handle-received-signaling-messages-for-call-reactions

Handle received signaling messages for call reactions
This commit is contained in:
Marcel Hibbe 2023-05-04 15:17:44 +02:00 committed by GitHub
commit 51bedbba9c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 180 additions and 14 deletions

View file

@ -296,6 +296,10 @@ public class CallActivity extends CallBaseActivity {
private Handler screenParticipantDisplayItemManagersHandler = new Handler(Looper.getMainLooper());
private Map<String, CallParticipantEventDisplayer> callParticipantEventDisplayers = new HashMap<>();
private Handler callParticipantEventDisplayersHandler = new Handler(Looper.getMainLooper());
private CallParticipantList.Observer callParticipantListObserver = new CallParticipantList.Observer() {
@Override
public void onCallParticipantsChanged(Collection<Participant> joined, Collection<Participant> updated,
@ -2248,6 +2252,11 @@ public class CallActivity extends CallBaseActivity {
screenParticipantDisplayItemManagers.put(sessionId, screenParticipantDisplayItemManager);
callParticipantModel.addObserver(screenParticipantDisplayItemManager, screenParticipantDisplayItemManagersHandler);
CallParticipantEventDisplayer callParticipantEventDisplayer =
new CallParticipantEventDisplayer(callParticipantModel);
callParticipantEventDisplayers.put(sessionId, callParticipantEventDisplayer);
callParticipantModel.addObserver(callParticipantEventDisplayer, callParticipantEventDisplayersHandler);
runOnUiThread(() -> {
addParticipantDisplayItem(callParticipantModel, "video");
});
@ -2288,6 +2297,10 @@ public class CallActivity extends CallBaseActivity {
screenParticipantDisplayItemManagers.remove(sessionId);
callParticipant.getCallParticipantModel().removeObserver(screenParticipantDisplayItemManager);
CallParticipantEventDisplayer callParticipantEventDisplayer =
callParticipantEventDisplayers.remove(sessionId);
callParticipant.getCallParticipantModel().removeObserver(callParticipantEventDisplayer);
callParticipant.destroy();
SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId);
@ -2793,16 +2806,10 @@ public class CallActivity extends CallBaseActivity {
@Override
public void onRaiseHand(boolean state, long timestamp) {
if (state) {
CallParticipant participant = callParticipants.get(sessionId);
if (participant != null) {
String nick = participant.getCallParticipantModel().getNick();
runOnUiThread(() -> Toast.makeText(
context,
String.format(context.getResources().getString(R.string.nc_call_raised_hand), nick),
Toast.LENGTH_LONG).show());
}
}
}
@Override
public void onReaction(String reaction) {
}
@Override
@ -2857,6 +2864,44 @@ public class CallActivity extends CallBaseActivity {
addParticipantDisplayItem(callParticipantModel, "screen");
}
}
@Override
public void onReaction(String reaction) {
}
}
private class CallParticipantEventDisplayer implements CallParticipantModel.Observer {
private final CallParticipantModel callParticipantModel;
private boolean raisedHand;
private CallParticipantEventDisplayer(CallParticipantModel callParticipantModel) {
this.callParticipantModel = callParticipantModel;
this.raisedHand = callParticipantModel.getRaisedHand() != null ?
callParticipantModel.getRaisedHand().getState() : false;
}
@Override
public void onChange() {
if (callParticipantModel.getRaisedHand() == null || !callParticipantModel.getRaisedHand().getState()) {
raisedHand = false;
return;
}
if (raisedHand) {
return;
}
raisedHand = true;
String nick = callParticipantModel.getNick();
Toast.makeText(context, String.format(context.getResources().getString(R.string.nc_call_raised_hand), nick), Toast.LENGTH_LONG).show();
}
@Override
public void onReaction(String reaction) {
}
}
private class InternalSignalingMessageSender implements SignalingMessageSender {

View file

@ -34,7 +34,16 @@ public class ParticipantDisplayItem {
private final CallParticipantModel callParticipantModel;
private final CallParticipantModel.Observer callParticipantModelObserver = this::updateFromModel;
private final CallParticipantModel.Observer callParticipantModelObserver = new CallParticipantModel.Observer() {
@Override
public void onChange() {
updateFromModel();
}
@Override
public void onReaction(String reaction) {
}
};
private String userId;
private PeerConnection.IceConnectionState iceConnectionState;

View file

@ -40,6 +40,11 @@ public class CallParticipant {
callParticipantModel.setRaisedHand(state, timestamp);
}
@Override
public void onReaction(String reaction) {
callParticipantModel.emitReaction(reaction);
}
@Override
public void onUnshareScreen() {
}

View file

@ -42,11 +42,15 @@ import java.util.Objects;
* Getters called after receiving a notification are guaranteed to provide at least the value that triggered the
* notification, but it may return even a more up to date one (so getting the value again on the following
* notification may return the same value as before).
*
* Besides onChange(), which notifies about changes in the model values, CallParticipantModel.Observer provides
* additional methods to be notified about one-time events that are not reflected in the model values, like reactions.
*/
public class CallParticipantModel {
public interface Observer {
void onChange();
void onReaction(String reaction);
}
protected class Data<T> {
@ -68,7 +72,7 @@ public class CallParticipantModel {
}
}
private final CallParticipantModelNotifier callParticipantModelNotifier = new CallParticipantModelNotifier();
protected final CallParticipantModelNotifier callParticipantModelNotifier = new CallParticipantModelNotifier();
protected final String sessionId;

View file

@ -83,4 +83,16 @@ class CallParticipantModelNotifier {
}
}
}
public synchronized void notifyReaction(String reaction) {
for (CallParticipantModelObserverOn observerOn : new ArrayList<>(callParticipantModelObserversOn)) {
if (observerOn.handler == null || observerOn.handler.getLooper() == Looper.myLooper()) {
observerOn.observer.onReaction(reaction);
} else {
observerOn.handler.post(() -> {
observerOn.observer.onReaction(reaction);
});
}
}
}
}

View file

@ -72,4 +72,8 @@ public class MutableCallParticipantModel extends CallParticipantModel {
public void setScreenMediaStream(MediaStream screenMediaStream) {
this.screenMediaStream.setValue(screenMediaStream);
}
public void emitReaction(String reaction) {
this.callParticipantModelNotifier.notifyReaction(reaction);
}
}

View file

@ -42,8 +42,10 @@ data class NCMessagePayload(
@JsonField(name = ["state"])
var state: Boolean? = null,
@JsonField(name = ["timestamp"])
var timestamp: Long? = null
var timestamp: Long? = null,
@JsonField(name = ["reaction"])
var reaction: String? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null, null, null, null, null, null)
constructor() : this(null, null, null, null, null, null, null, null)
}

View file

@ -93,6 +93,12 @@ class CallParticipantMessageNotifier {
}
}
public void notifyReaction(String sessionId, String reaction) {
for (SignalingMessageReceiver.CallParticipantMessageListener listener : getListenersFor(sessionId)) {
listener.onReaction(reaction);
}
}
public synchronized void notifyUnshareScreen(String sessionId) {
for (SignalingMessageReceiver.CallParticipantMessageListener listener : getListenersFor(sessionId)) {
listener.onUnshareScreen();

View file

@ -149,6 +149,7 @@ public abstract class SignalingMessageReceiver {
*/
public interface CallParticipantMessageListener {
void onRaiseHand(boolean state, long timestamp);
void onReaction(String reaction);
void onUnshareScreen();
}
@ -562,6 +563,57 @@ public abstract class SignalingMessageReceiver {
return;
}
if ("reaction".equals(type)) {
// Message schema (external signaling server):
// {
// "type": "message",
// "message": {
// "sender": {
// ...
// },
// "data": {
// "to": #STRING#,
// "roomType": "video",
// "type": "reaction",
// "payload": {
// "reaction": #STRING#,
// },
// "from": #STRING#,
// },
// },
// }
//
// Message schema (internal signaling server):
// {
// "type": "message",
// "data": {
// "to": #STRING#,
// "roomType": "video",
// "type": "reaction",
// "payload": {
// "reaction": #STRING#,
// },
// "from": #STRING#,
// },
// }
NCMessagePayload payload = signalingMessage.getPayload();
if (payload == null) {
// Broken message, this should not happen.
return;
}
String reaction = payload.getReaction();
if (reaction == null) {
// Broken message, this should not happen.
return;
}
callParticipantMessageNotifier.notifyReaction(sessionId, reaction);
return;
}
// "unshareScreen" messages are directly sent to the screen peer connection when the internal signaling
// server is used, and to the room when the external signaling server is used. However, the (relevant) data
// of the received message ("from" and "type") is the same in both cases.

View file

@ -55,4 +55,11 @@ class CallParticipantModelTest {
callParticipantModel!!.setRaisedHand(true, 4815162342L)
Mockito.verify(mockedCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testEmitReaction() {
callParticipantModel!!.addObserver(mockedCallParticipantModelObserver)
callParticipantModel!!.emitReaction("theReaction")
Mockito.verify(mockedCallParticipantModelObserver, Mockito.only())?.onReaction("theReaction")
}
}

View file

@ -84,6 +84,26 @@ public class SignalingMessageReceiverCallParticipantTest {
verify(mockedCallParticipantMessageListener, only()).onRaiseHand(true, 4815162342L);
}
@Test
public void testCallParticipantMessageReaction() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId");
NCSignalingMessage signalingMessage = new NCSignalingMessage();
signalingMessage.setFrom("theSessionId");
signalingMessage.setType("reaction");
signalingMessage.setRoomType("theRoomType");
NCMessagePayload messagePayload = new NCMessagePayload();
messagePayload.setType("reaction");
messagePayload.setReaction("theReaction");
signalingMessage.setPayload(messagePayload);
signalingMessageReceiver.processSignalingMessage(signalingMessage);
verify(mockedCallParticipantMessageListener, only()).onReaction("theReaction");
}
@Test
public void testCallParticipantMessageUnshareScreen() {
SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =