Merge pull request #2545 from nextcloud/add-listener-for-data-channel-messages

Add listener for data channel messages
This commit is contained in:
Tim Krüger 2022-12-28 15:30:20 +01:00 committed by GitHub
commit 5786baaeb7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 312 additions and 139 deletions

View file

@ -76,7 +76,6 @@ import com.nextcloud.talk.models.json.generic.GenericOverall;
import com.nextcloud.talk.models.json.participants.Participant; import com.nextcloud.talk.models.json.participants.Participant;
import com.nextcloud.talk.models.json.participants.ParticipantsOverall; import com.nextcloud.talk.models.json.participants.ParticipantsOverall;
import com.nextcloud.talk.models.json.signaling.DataChannelMessage; import com.nextcloud.talk.models.json.signaling.DataChannelMessage;
import com.nextcloud.talk.models.json.signaling.DataChannelMessageNick;
import com.nextcloud.talk.models.json.signaling.NCMessagePayload; import com.nextcloud.talk.models.json.signaling.NCMessagePayload;
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
import com.nextcloud.talk.models.json.signaling.Signaling; import com.nextcloud.talk.models.json.signaling.Signaling;
@ -264,9 +263,13 @@ public class CallActivity extends CallBaseActivity {
private InternalSignalingMessageSender internalSignalingMessageSender = new InternalSignalingMessageSender(); private InternalSignalingMessageSender internalSignalingMessageSender = new InternalSignalingMessageSender();
private SignalingMessageSender signalingMessageSender; private SignalingMessageSender signalingMessageSender;
private Map<String, OfferAnswerNickProvider> offerAnswerNickProviders = new HashMap<>();
private Map<String, SignalingMessageReceiver.CallParticipantMessageListener> callParticipantMessageListeners = private Map<String, SignalingMessageReceiver.CallParticipantMessageListener> callParticipantMessageListeners =
new HashMap<>(); new HashMap<>();
private Map<String, PeerConnectionWrapper.DataChannelMessageListener> dataChannelMessageListeners = new HashMap<>();
private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener = new SignalingMessageReceiver.ParticipantListMessageListener() { private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener = new SignalingMessageReceiver.ParticipantListMessageListener() {
@Override @Override
@ -2006,6 +2009,19 @@ public class CallActivity extends CallBaseActivity {
new CallActivityCallParticipantMessageListener(sessionId); new CallActivityCallParticipantMessageListener(sessionId);
callParticipantMessageListeners.put(sessionId, callParticipantMessageListener); callParticipantMessageListeners.put(sessionId, callParticipantMessageListener);
signalingMessageReceiver.addListener(callParticipantMessageListener, sessionId); signalingMessageReceiver.addListener(callParticipantMessageListener, sessionId);
// DataChannel messages are sent only in video peers; (sender) screen peers do not even open them.
PeerConnectionWrapper.DataChannelMessageListener dataChannelMessageListener =
new CallActivityDataChannelMessageListener(sessionId);
dataChannelMessageListeners.put(sessionId, dataChannelMessageListener);
peerConnectionWrapper.addListener(dataChannelMessageListener);
}
if (!publisher && !hasExternalSignalingServer && offerAnswerNickProviders.get(sessionId) == null) {
OfferAnswerNickProvider offerAnswerNickProvider = new OfferAnswerNickProvider();
offerAnswerNickProviders.put(sessionId, offerAnswerNickProvider);
signalingMessageReceiver.addListener(offerAnswerNickProvider.getVideoWebRtcMessageListener(), sessionId, "video");
signalingMessageReceiver.addListener(offerAnswerNickProvider.getScreenWebRtcMessageListener(), sessionId, "screen");
} }
if (publisher) { if (publisher) {
@ -2032,6 +2048,10 @@ public class CallActivity extends CallBaseActivity {
if (!(peerConnectionWrappers = getPeerConnectionWrapperListForSessionId(sessionId)).isEmpty()) { if (!(peerConnectionWrappers = getPeerConnectionWrapperListForSessionId(sessionId)).isEmpty()) {
for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrappers) { for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrappers) {
if (peerConnectionWrapper.getSessionId().equals(sessionId)) { if (peerConnectionWrapper.getSessionId().equals(sessionId)) {
if (!justScreen && VIDEO_STREAM_TYPE_VIDEO.equals(peerConnectionWrapper.getVideoStreamType())) {
PeerConnectionWrapper.DataChannelMessageListener dataChannelMessageListener = dataChannelMessageListeners.remove(sessionId);
peerConnectionWrapper.removeListener(dataChannelMessageListener);
}
String videoStreamType = peerConnectionWrapper.getVideoStreamType(); String videoStreamType = peerConnectionWrapper.getVideoStreamType();
if (VIDEO_STREAM_TYPE_SCREEN.equals(videoStreamType) || !justScreen) { if (VIDEO_STREAM_TYPE_SCREEN.equals(videoStreamType) || !justScreen) {
runOnUiThread(() -> removeMediaStream(sessionId, videoStreamType)); runOnUiThread(() -> removeMediaStream(sessionId, videoStreamType));
@ -2044,6 +2064,12 @@ public class CallActivity extends CallBaseActivity {
if (!justScreen) { if (!justScreen) {
SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId); SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId);
signalingMessageReceiver.removeListener(listener); signalingMessageReceiver.removeListener(listener);
OfferAnswerNickProvider offerAnswerNickProvider = offerAnswerNickProviders.remove(sessionId);
if (offerAnswerNickProvider != null) {
signalingMessageReceiver.removeListener(offerAnswerNickProvider.getVideoWebRtcMessageListener());
signalingMessageReceiver.removeListener(offerAnswerNickProvider.getScreenWebRtcMessageListener());
}
} }
} }
@ -2149,24 +2175,6 @@ public class CallActivity extends CallBaseActivity {
toggleMedia(enableVideo, true); toggleMedia(enableVideo, true);
} }
} }
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
PeerConnectionEvent.PeerConnectionEventType.NICK_CHANGE) {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setNick(peerConnectionEvent.getNick());
participantsAdapter.notifyDataSetChanged();
}
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
PeerConnectionEvent.PeerConnectionEventType.VIDEO_CHANGE && !isVoiceOnlyCall) {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setStreamEnabled(peerConnectionEvent.getChangeValue());
participantsAdapter.notifyDataSetChanged();
}
} else if (peerConnectionEvent.getPeerConnectionEventType() ==
PeerConnectionEvent.PeerConnectionEventType.AUDIO_CHANGE) {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setAudioEnabled(peerConnectionEvent.getChangeValue());
participantsAdapter.notifyDataSetChanged();
}
} else if (peerConnectionEvent.getPeerConnectionEventType() == } else if (peerConnectionEvent.getPeerConnectionEventType() ==
PeerConnectionEvent.PeerConnectionEventType.PUBLISHER_FAILED) { PeerConnectionEvent.PeerConnectionEventType.PUBLISHER_FAILED) {
setCallState(CallStatus.PUBLISHER_FAILED); setCallState(CallStatus.PUBLISHER_FAILED);
@ -2176,12 +2184,12 @@ public class CallActivity extends CallBaseActivity {
} }
private void startSendingNick() { private void startSendingNick() {
DataChannelMessageNick dataChannelMessage = new DataChannelMessageNick(); DataChannelMessage dataChannelMessage = new DataChannelMessage();
dataChannelMessage.setType("nickChanged"); dataChannelMessage.setType("nickChanged");
HashMap<String, String> nickChangedPayload = new HashMap<>(); Map<String, String> nickChangedPayload = new HashMap<>();
nickChangedPayload.put("userid", conversationUser.getUserId()); nickChangedPayload.put("userid", conversationUser.getUserId());
nickChangedPayload.put("name", conversationUser.getDisplayName()); nickChangedPayload.put("name", conversationUser.getDisplayName());
dataChannelMessage.setPayload(nickChangedPayload); dataChannelMessage.setPayloadMap(nickChangedPayload);
for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrapperList) { for (PeerConnectionWrapper peerConnectionWrapper : peerConnectionWrapperList) {
if (peerConnectionWrapper.isMCUPublisher()) { if (peerConnectionWrapper.isMCUPublisher()) {
Observable Observable
@ -2196,7 +2204,7 @@ public class CallActivity extends CallBaseActivity {
@Override @Override
public void onNext(@io.reactivex.annotations.NonNull Long aLong) { public void onNext(@io.reactivex.annotations.NonNull Long aLong) {
peerConnectionWrapper.sendNickChannelData(dataChannelMessage); peerConnectionWrapper.sendChannelData(dataChannelMessage);
} }
@Override @Override
@ -2265,7 +2273,7 @@ public class CallActivity extends CallBaseActivity {
if (hasExternalSignalingServer) { if (hasExternalSignalingServer) {
nick = webSocketClient.getDisplayNameForSession(session); nick = webSocketClient.getDisplayNameForSession(session);
} else { } else {
nick = peerConnectionWrapper != null ? peerConnectionWrapper.getNick() : ""; nick = offerAnswerNickProviders.get(session) != null ? offerAnswerNickProviders.get(session).getNick() : "";
} }
String userId4Usage = userId; String userId4Usage = userId;
@ -2278,11 +2286,14 @@ public class CallActivity extends CallBaseActivity {
} }
} }
String defaultGuestNick = getResources().getString(R.string.nc_nick_guest);
ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(baseUrl, ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(baseUrl,
userId4Usage, userId4Usage,
session, session,
connected, connected,
nick, nick,
defaultGuestNick,
mediaStream, mediaStream,
videoStreamType, videoStreamType,
videoStreamEnabled, videoStreamEnabled,
@ -2559,6 +2570,47 @@ public class CallActivity extends CallBaseActivity {
} }
} }
private static class OfferAnswerNickProvider {
private class WebRtcMessageListener implements SignalingMessageReceiver.WebRtcMessageListener {
@Override
public void onOffer(String sdp, String nick) {
(OfferAnswerNickProvider.this).nick = nick;
}
@Override
public void onAnswer(String sdp, String nick) {
(OfferAnswerNickProvider.this).nick = nick;
}
@Override
public void onCandidate(String sdpMid, int sdpMLineIndex, String sdp) {
}
@Override
public void onEndOfCandidates() {
}
}
private final WebRtcMessageListener videoWebRtcMessageListener = new WebRtcMessageListener();
private final WebRtcMessageListener screenWebRtcMessageListener = new WebRtcMessageListener();
private String nick;
public WebRtcMessageListener getVideoWebRtcMessageListener() {
return videoWebRtcMessageListener;
}
public WebRtcMessageListener getScreenWebRtcMessageListener() {
return screenWebRtcMessageListener;
}
public String getNick() {
return nick;
}
}
private class CallActivityCallParticipantMessageListener implements SignalingMessageReceiver.CallParticipantMessageListener { private class CallActivityCallParticipantMessageListener implements SignalingMessageReceiver.CallParticipantMessageListener {
private final String sessionId; private final String sessionId;
@ -2573,6 +2625,66 @@ public class CallActivity extends CallBaseActivity {
} }
} }
private class CallActivityDataChannelMessageListener implements PeerConnectionWrapper.DataChannelMessageListener {
private final String participantDisplayItemId;
private CallActivityDataChannelMessageListener(String sessionId) {
// DataChannel messages are sent only in video peers, so the listener only acts on the "video" items.
this.participantDisplayItemId = sessionId + "-video";
}
@Override
public void onAudioOn() {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setAudioEnabled(true);
participantsAdapter.notifyDataSetChanged();
}
});
}
@Override
public void onAudioOff() {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setAudioEnabled(false);
participantsAdapter.notifyDataSetChanged();
}
});
}
@Override
public void onVideoOn() {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setStreamEnabled(true);
participantsAdapter.notifyDataSetChanged();
}
});
}
@Override
public void onVideoOff() {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setStreamEnabled(false);
participantsAdapter.notifyDataSetChanged();
}
});
}
@Override
public void onNickChanged(String nick) {
runOnUiThread(() -> {
if (participantDisplayItems.get(participantDisplayItemId) != null) {
participantDisplayItems.get(participantDisplayItemId).setNick(nick);
participantsAdapter.notifyDataSetChanged();
}
});
}
}
private class InternalSignalingMessageSender implements SignalingMessageSender { private class InternalSignalingMessageSender implements SignalingMessageSender {
@Override @Override

View file

@ -13,6 +13,7 @@ public class ParticipantDisplayItem {
private String session; private String session;
private boolean connected; private boolean connected;
private String nick; private String nick;
private final String defaultGuestNick;
private String urlForAvatar; private String urlForAvatar;
private MediaStream mediaStream; private MediaStream mediaStream;
private String streamType; private String streamType;
@ -20,12 +21,13 @@ public class ParticipantDisplayItem {
private EglBase rootEglBase; private EglBase rootEglBase;
private boolean isAudioEnabled; private boolean isAudioEnabled;
public ParticipantDisplayItem(String baseUrl, String userId, String session, boolean connected, String nick, MediaStream mediaStream, String streamType, boolean streamEnabled, EglBase rootEglBase) { public ParticipantDisplayItem(String baseUrl, String userId, String session, boolean connected, String nick, String defaultGuestNick, MediaStream mediaStream, String streamType, boolean streamEnabled, EglBase rootEglBase) {
this.baseUrl = baseUrl; this.baseUrl = baseUrl;
this.userId = userId; this.userId = userId;
this.session = session; this.session = session;
this.connected = connected; this.connected = connected;
this.nick = nick; this.nick = nick;
this.defaultGuestNick = defaultGuestNick;
this.mediaStream = mediaStream; this.mediaStream = mediaStream;
this.streamType = streamType; this.streamType = streamType;
this.streamEnabled = streamEnabled; this.streamEnabled = streamEnabled;
@ -61,6 +63,10 @@ public class ParticipantDisplayItem {
} }
public String getNick() { public String getNick() {
if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(nick)) {
return defaultGuestNick;
}
return nick; return nick;
} }
@ -78,7 +84,7 @@ public class ParticipantDisplayItem {
if (!TextUtils.isEmpty(userId)) { if (!TextUtils.isEmpty(userId)) {
urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true); urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true);
} else { } else {
urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl, nick, true); urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl, getNick(), true);
} }
} }

View file

@ -120,6 +120,6 @@ public class PeerConnectionEvent {
} }
public enum PeerConnectionEventType { public enum PeerConnectionEventType {
PEER_CONNECTED, PEER_DISCONNECTED, PEER_CLOSED, SENSOR_FAR, SENSOR_NEAR, NICK_CHANGE, AUDIO_CHANGE, VIDEO_CHANGE, PUBLISHER_FAILED PEER_CONNECTED, PEER_DISCONNECTED, PEER_CLOSED, SENSOR_FAR, SENSOR_NEAR, PUBLISHER_FAILED
} }
} }

View file

@ -34,10 +34,15 @@ import kotlinx.android.parcel.TypeParceler
data class DataChannelMessage( data class DataChannelMessage(
@JsonField(name = ["type"]) @JsonField(name = ["type"])
var type: String? = null, var type: String? = null,
/** Can be String or Map<String, String>
* Use only for received messages */
@JsonField(name = ["payload"]) @JsonField(name = ["payload"])
var payload: Any? = null var payload: Any? = null,
/** Use only to send messages */
@JsonField(name = ["payload"])
var payloadMap: Map<String, String>? = null
) : Parcelable { ) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject' // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null) constructor() : this(null, null, null)
constructor(type: String) : this(type, null) constructor(type: String) : this(type, null)
} }

View file

@ -1,40 +0,0 @@
/*
* Nextcloud Talk application
*
* @author Mario Danic
* @author Andy Scherzinger
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
* Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.models.json.signaling
import android.os.Parcelable
import com.bluelinelabs.logansquare.annotation.JsonField
import com.bluelinelabs.logansquare.annotation.JsonObject
import java.util.HashMap
import kotlinx.android.parcel.Parcelize
@Parcelize
@JsonObject
data class DataChannelMessageNick(
@JsonField(name = ["type"])
var type: String? = null,
@JsonField(name = ["payload"])
var payload: HashMap<String, String>? = null
) : Parcelable {
// This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
constructor() : this(null, null)
}

View file

@ -0,0 +1,77 @@
/*
* Nextcloud Talk application
*
* @author Daniel Calviño Sánchez
* Copyright (C) 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.nextcloud.talk.webrtc;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.Set;
/**
* Helper class to register and notify DataChannelMessageListeners.
*
* This class is only meant for internal use by PeerConnectionWrapper; listeners must register themselves against
* a PeerConnectionWrapper rather than against a DataChannelMessageNotifier.
*/
public class DataChannelMessageNotifier {
private final Set<PeerConnectionWrapper.DataChannelMessageListener> dataChannelMessageListeners = new LinkedHashSet<>();
public synchronized void addListener(PeerConnectionWrapper.DataChannelMessageListener listener) {
if (listener == null) {
throw new IllegalArgumentException("DataChannelMessageListener can not be null");
}
dataChannelMessageListeners.add(listener);
}
public synchronized void removeListener(PeerConnectionWrapper.DataChannelMessageListener listener) {
dataChannelMessageListeners.remove(listener);
}
public synchronized void notifyAudioOn() {
for (PeerConnectionWrapper.DataChannelMessageListener listener : new ArrayList<>(dataChannelMessageListeners)) {
listener.onAudioOn();
}
}
public synchronized void notifyAudioOff() {
for (PeerConnectionWrapper.DataChannelMessageListener listener : new ArrayList<>(dataChannelMessageListeners)) {
listener.onAudioOff();
}
}
public synchronized void notifyVideoOn() {
for (PeerConnectionWrapper.DataChannelMessageListener listener : new ArrayList<>(dataChannelMessageListeners)) {
listener.onVideoOn();
}
}
public synchronized void notifyVideoOff() {
for (PeerConnectionWrapper.DataChannelMessageListener listener : new ArrayList<>(dataChannelMessageListeners)) {
listener.onVideoOff();
}
}
public synchronized void notifyNickChanged(String nick) {
for (PeerConnectionWrapper.DataChannelMessageListener listener : new ArrayList<>(dataChannelMessageListeners)) {
listener.onNickChanged(nick);
}
}
}

View file

@ -25,7 +25,6 @@ import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.bluelinelabs.logansquare.LoganSquare; import com.bluelinelabs.logansquare.LoganSquare;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.data.user.model.User; import com.nextcloud.talk.data.user.model.User;
import com.nextcloud.talk.events.NetworkEvent; import com.nextcloud.talk.events.NetworkEvent;
@ -384,7 +383,7 @@ public class MagicWebSocketInstance extends WebSocketListener {
} }
} }
return NextcloudTalkApplication.Companion.getSharedApplication().getString(R.string.nc_nick_guest); return "";
} }
public String getUserIdForSession(String session) { public String getUserIdForSession(String session) {

View file

@ -24,16 +24,13 @@
package com.nextcloud.talk.webrtc; package com.nextcloud.talk.webrtc;
import android.content.Context; import android.content.Context;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import com.bluelinelabs.logansquare.LoganSquare; import com.bluelinelabs.logansquare.LoganSquare;
import com.nextcloud.talk.R;
import com.nextcloud.talk.application.NextcloudTalkApplication; import com.nextcloud.talk.application.NextcloudTalkApplication;
import com.nextcloud.talk.events.MediaStreamEvent; import com.nextcloud.talk.events.MediaStreamEvent;
import com.nextcloud.talk.events.PeerConnectionEvent; import com.nextcloud.talk.events.PeerConnectionEvent;
import com.nextcloud.talk.models.json.signaling.DataChannelMessage; import com.nextcloud.talk.models.json.signaling.DataChannelMessage;
import com.nextcloud.talk.models.json.signaling.DataChannelMessageNick;
import com.nextcloud.talk.models.json.signaling.NCIceCandidate; import com.nextcloud.talk.models.json.signaling.NCIceCandidate;
import com.nextcloud.talk.models.json.signaling.NCMessagePayload; import com.nextcloud.talk.models.json.signaling.NCMessagePayload;
import com.nextcloud.talk.models.json.signaling.NCSignalingMessage; import com.nextcloud.talk.models.json.signaling.NCSignalingMessage;
@ -59,8 +56,8 @@ import java.io.IOException;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import javax.inject.Inject; import javax.inject.Inject;
@ -68,12 +65,26 @@ import javax.inject.Inject;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import autodagger.AutoInjector; import autodagger.AutoInjector;
import static java.lang.Boolean.FALSE;
import static java.lang.Boolean.TRUE;
@AutoInjector(NextcloudTalkApplication.class) @AutoInjector(NextcloudTalkApplication.class)
public class PeerConnectionWrapper { public class PeerConnectionWrapper {
/**
* Listener for data channel messages.
*
* The messages are bound to a specific peer connection, so each listener is expected to handle messages only for
* a single peer connection.
*
* All methods are called on the so called "signaling" thread of WebRTC, which is an internal thread created by the
* WebRTC library and NOT the same thread where signaling messages are received.
*/
public interface DataChannelMessageListener {
void onAudioOn();
void onAudioOff();
void onVideoOn();
void onVideoOff();
void onNickChanged(String nick);
}
private static final String TAG = PeerConnectionWrapper.class.getCanonicalName(); private static final String TAG = PeerConnectionWrapper.class.getCanonicalName();
private final SignalingMessageReceiver signalingMessageReceiver; private final SignalingMessageReceiver signalingMessageReceiver;
@ -81,10 +92,11 @@ public class PeerConnectionWrapper {
private final SignalingMessageSender signalingMessageSender; private final SignalingMessageSender signalingMessageSender;
private final DataChannelMessageNotifier dataChannelMessageNotifier = new DataChannelMessageNotifier();
private List<IceCandidate> iceCandidates = new ArrayList<>(); private List<IceCandidate> iceCandidates = new ArrayList<>();
private PeerConnection peerConnection; private PeerConnection peerConnection;
private String sessionId; private String sessionId;
private String nick;
private final MediaConstraints mediaConstraints; private final MediaConstraints mediaConstraints;
private DataChannel dataChannel; private DataChannel dataChannel;
private final MagicSdpObserver magicSdpObserver; private final MagicSdpObserver magicSdpObserver;
@ -160,6 +172,21 @@ public class PeerConnectionWrapper {
} }
} }
/**
* Adds a listener for data channel messages.
*
* A listener is expected to be added only once. If the same listener is added again it will be notified just once.
*
* @param listener the DataChannelMessageListener
*/
public void addListener(DataChannelMessageListener listener) {
dataChannelMessageNotifier.addListener(listener);
}
public void removeListener(DataChannelMessageListener listener) {
dataChannelMessageNotifier.removeListener(listener);
}
public String getVideoStreamType() { public String getVideoStreamType() {
return videoStreamType; return videoStreamType;
} }
@ -203,18 +230,6 @@ public class PeerConnectionWrapper {
} }
} }
public void sendNickChannelData(DataChannelMessageNick dataChannelMessage) {
ByteBuffer buffer;
if (dataChannel != null) {
try {
buffer = ByteBuffer.wrap(LoganSquare.serialize(dataChannelMessage).getBytes());
dataChannel.send(new DataChannel.Buffer(buffer, false));
} catch (IOException e) {
Log.d(TAG, "Failed to send channel data, attempting regular " + dataChannelMessage);
}
}
}
public void sendChannelData(DataChannelMessage dataChannelMessage) { public void sendChannelData(DataChannelMessage dataChannelMessage) {
ByteBuffer buffer; ByteBuffer buffer;
if (dataChannel != null) { if (dataChannel != null) {
@ -235,18 +250,6 @@ public class PeerConnectionWrapper {
return sessionId; return sessionId;
} }
public String getNick() {
if (!TextUtils.isEmpty(nick)) {
return nick;
} else {
return Objects.requireNonNull(NextcloudTalkApplication.Companion.getSharedApplication()).getString(R.string.nc_nick_guest);
}
}
private void setNick(String nick) {
this.nick = nick;
}
private void sendInitialMediaStatus() { private void sendInitialMediaStatus() {
if (localStream != null) { if (localStream != null) {
if (localStream.videoTracks.size() == 1 && localStream.videoTracks.get(0).enabled()) { if (localStream.videoTracks.size() == 1 && localStream.videoTracks.get(0).enabled()) {
@ -288,16 +291,14 @@ public class PeerConnectionWrapper {
private class WebRtcMessageListener implements SignalingMessageReceiver.WebRtcMessageListener { private class WebRtcMessageListener implements SignalingMessageReceiver.WebRtcMessageListener {
public void onOffer(String sdp, String nick) { public void onOffer(String sdp, String nick) {
onOfferOrAnswer("offer", sdp, nick); onOfferOrAnswer("offer", sdp);
} }
public void onAnswer(String sdp, String nick) { public void onAnswer(String sdp, String nick) {
onOfferOrAnswer("answer", sdp, nick); onOfferOrAnswer("answer", sdp);
} }
private void onOfferOrAnswer(String type, String sdp, String nick) { private void onOfferOrAnswer(String type, String sdp) {
setNick(nick);
SessionDescription sessionDescriptionWithPreferredCodec; SessionDescription sessionDescriptionWithPreferredCodec;
boolean isAudio = false; boolean isAudio = false;
@ -350,40 +351,53 @@ public class PeerConnectionWrapper {
String strData = new String(bytes); String strData = new String(bytes);
Log.d(TAG, "Got msg: " + strData + " over " + TAG + " " + sessionId); Log.d(TAG, "Got msg: " + strData + " over " + TAG + " " + sessionId);
DataChannelMessage dataChannelMessage;
try { try {
DataChannelMessage dataChannelMessage = LoganSquare.parse(strData, DataChannelMessage.class); dataChannelMessage = LoganSquare.parse(strData, DataChannelMessage.class);
String internalNick;
if ("nickChanged".equals(dataChannelMessage.getType())) {
if (dataChannelMessage.getPayload() instanceof String) {
internalNick = (String) dataChannelMessage.getPayload();
if (!internalNick.equals(nick)) {
setNick(internalNick);
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
.NICK_CHANGE, sessionId, getNick(), null, videoStreamType));
}
} else {
if (dataChannelMessage.getPayload() != null) {
HashMap<String, String> payloadHashMap = (HashMap<String, String>) dataChannelMessage.getPayload();
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
.NICK_CHANGE, sessionId, payloadHashMap.get("name"), null, videoStreamType));
}
}
} else if ("audioOn".equals(dataChannelMessage.getType())) {
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
.AUDIO_CHANGE, sessionId, null, TRUE, videoStreamType));
} else if ("audioOff".equals(dataChannelMessage.getType())) {
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
.AUDIO_CHANGE, sessionId, null, FALSE, videoStreamType));
} else if ("videoOn".equals(dataChannelMessage.getType())) {
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
.VIDEO_CHANGE, sessionId, null, TRUE, videoStreamType));
} else if ("videoOff".equals(dataChannelMessage.getType())) {
EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
.VIDEO_CHANGE, sessionId, null, FALSE, videoStreamType));
}
} catch (IOException e) { } catch (IOException e) {
Log.d(TAG, "Failed to parse data channel message"); Log.d(TAG, "Failed to parse data channel message");
return;
}
if ("nickChanged".equals(dataChannelMessage.getType())) {
String nick = null;
if (dataChannelMessage.getPayload() instanceof String) {
nick = (String) dataChannelMessage.getPayload();
} else if (dataChannelMessage.getPayload() instanceof Map) {
Map<String, String> payloadMap = (Map<String, String>) dataChannelMessage.getPayload();
nick = payloadMap.get("name");
}
if (nick != null) {
dataChannelMessageNotifier.notifyNickChanged(nick);
}
return;
}
if ("audioOn".equals(dataChannelMessage.getType())) {
dataChannelMessageNotifier.notifyAudioOn();
return;
}
if ("audioOff".equals(dataChannelMessage.getType())) {
dataChannelMessageNotifier.notifyAudioOff();
return;
}
if ("videoOn".equals(dataChannelMessage.getType())) {
dataChannelMessageNotifier.notifyVideoOn();
return;
}
if ("videoOff".equals(dataChannelMessage.getType())) {
dataChannelMessageNotifier.notifyVideoOff();
return;
} }
} }
} }