prepare JingleRtpConnection for content-adds & add switch to video menu item to call

This commit is contained in:
Daniel Gultsch 2022-11-28 11:39:26 +01:00 committed by Arne
parent 6741547102
commit be9a38d3ee
16 changed files with 698 additions and 252 deletions

View file

@ -107,13 +107,13 @@ android {
minSdkVersion 21
targetSdkVersion 32
//versionNameSuffix " beta_(2022-11-23)" // " beta_(XXXX-XX-XX)" // activate for beta versions
//versionNameSuffix " beta_(2022-11-29)" // " beta_(XXXX-XX-XX)" // activate for beta versions
versionCode 125
versionName "1.5.14"
//resConfigs "en"
archivesBaseName += "-$versionName"
archivesBaseName += "$versionNameSuffix" // activate for beta versions
//archivesBaseName += "$versionNameSuffix" // activate for beta versions
applicationId "de.monocles.chat"
multiDexEnabled true

View file

@ -21,7 +21,7 @@ import android.media.AudioRecord;
import android.media.MediaRecorder;
import android.os.Build;
import android.util.Log;
import eu.siacs.conversations.xmpp.jingle.Media;
import androidx.annotation.Nullable;
import org.webrtc.ThreadUtils;
@ -44,7 +44,7 @@ public class AppRTCAudioManager {
private final Context apprtcContext;
// Contains speakerphone setting: auto, true or false
@Nullable
private final SpeakerPhonePreference speakerPhonePreference;
private SpeakerPhonePreference speakerPhonePreference;
// Handles all tasks related to Bluetooth headset devices.
private final AppRTCBluetoothManager bluetoothManager;
@Nullable
@ -109,6 +109,16 @@ public class AppRTCAudioManager {
Log.d(Config.LOGTAG, "defaultAudioDevice: " + defaultAudioDevice);
AppRTCUtils.logDeviceInfo(Config.LOGTAG);
}
public void switchSpeakerPhonePreference(final SpeakerPhonePreference speakerPhonePreference) {
this.speakerPhonePreference = speakerPhonePreference;
if (speakerPhonePreference == SpeakerPhonePreference.EARPIECE && hasEarpiece()) {
defaultAudioDevice = AudioDevice.EARPIECE;
} else {
defaultAudioDevice = AudioDevice.SPEAKER_PHONE;
}
updateAudioDeviceState();
}
/**
* Construction.
@ -587,7 +597,15 @@ public class AppRTCAudioManager {
}
public enum SpeakerPhonePreference {
AUTO, EARPIECE, SPEAKER
AUTO, EARPIECE, SPEAKER;
public static SpeakerPhonePreference of(final Set<Media> media) {
if (media.contains(Media.VIDEO)) {
return SPEAKER;
} else {
return EARPIECE;
}
}
}
/**

View file

@ -15,6 +15,7 @@ import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Typeface;
import android.media.AudioAttributes;
import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
@ -50,6 +51,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@ -122,6 +124,10 @@ public class NotificationService {
private boolean mIsInForeground;
private long mLastNotification;
private static final String INCOMING_CALLS_NOTIFICATION_CHANNEL = "incoming_calls_channel";
private Ringtone currentlyPlayingRingtone = null;
private ScheduledFuture<?> vibrationFuture;
NotificationService(final XmppConnectionService service) {
this.mXmppConnectionService = service;
}
@ -788,9 +794,27 @@ public class NotificationService {
}
public void cancelIncomingCallNotification() {
stopSoundAndVibration();
cancel(INCOMING_CALL_NOTIFICATION_ID);
}
public boolean stopSoundAndVibration() {
int stopped = 0;
if (this.currentlyPlayingRingtone != null) {
if (this.currentlyPlayingRingtone.isPlaying()) {
Log.d(Config.LOGTAG, "stop playing ring tone");
++stopped;
}
this.currentlyPlayingRingtone.stop();
}
if (this.vibrationFuture != null && !this.vibrationFuture.isCancelled()) {
Log.d(Config.LOGTAG, "stop vibration");
this.vibrationFuture.cancel(true);
++stopped;
}
return stopped > 0;
}
public static void cancelIncomingCallNotification(final Context context) {
final NotificationManagerCompat notificationManager = NotificationManagerCompat.from(context);
try {

View file

@ -18,16 +18,18 @@ import android.os.PowerManager;
import android.os.SystemClock;
import android.util.Log;
import android.util.Rational;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.StringRes;
import androidx.databinding.DataBindingUtil;
import androidx.annotation.Nullable;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
@ -58,21 +60,19 @@ import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.util.AvatarWorkerTask;
import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.ui.util.Rationals;
import eu.siacs.conversations.utils.Compatibility;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.utils.PermissionUtils;
import eu.siacs.conversations.utils.TimeFrameUtils;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
import me.drakeet.support.toast.ToastCompat;
public class RtpSessionActivity extends XmppActivity
implements XmppConnectionService.OnJingleRtpConnectionUpdate,
eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
public static final String EXTRA_WITH = "with";
public static final String EXTRA_SESSION_ID = "session_id";
@ -169,6 +169,18 @@ public class RtpSessionActivity extends XmppActivity
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onKeyDown(final int keyCode, final KeyEvent event) {
if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
if (xmppConnectionService != null) {
if (xmppConnectionService.getNotificationService().stopSoundAndVibration()) {
return true;
}
}
}
return super.onKeyDown(keyCode, event);
}
private boolean isHelpButtonVisible() {
try {
return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState());
@ -187,8 +199,8 @@ public class RtpSessionActivity extends XmppActivity
private boolean isSwitchToConversationVisible() {
final JingleRtpConnection connection =
this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
return connection != null && !connection.getMedia().contains(Media.VIDEO);
return connection != null
&& STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState());
}
private void switchToConversation() {
@ -216,7 +228,7 @@ public class RtpSessionActivity extends XmppActivity
try {
startActivity(intent);
} catch (final ActivityNotFoundException e) {
ToastCompat.makeText(this, R.string.no_application_found_to_open_link, ToastCompat.LENGTH_LONG)
Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG)
.show();
}
}
@ -281,7 +293,7 @@ public class RtpSessionActivity extends XmppActivity
try {
requireRtpConnection().acceptCall();
} catch (final IllegalStateException e) {
ToastCompat.makeText(this, e.getMessage(), ToastCompat.LENGTH_SHORT).show();
Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
}
}
@ -299,7 +311,6 @@ public class RtpSessionActivity extends XmppActivity
@Override
public void run() {
new Thread(() -> {
final long start = SystemClock.elapsedRealtime();
final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable();
final long stop = SystemClock.elapsedRealtime();
@ -311,11 +322,13 @@ public class RtpSessionActivity extends XmppActivity
if (activity == null) {
return;
}
activity.runOnUiThread(() -> ToastCompat.makeText(
activity.runOnUiThread(
() ->
Toast.makeText(
activity,
R.string.microphone_unavailable,
ToastCompat.LENGTH_LONG).show());
}).start();
Toast.LENGTH_LONG)
.show());
}
}
@ -332,7 +345,7 @@ public class RtpSessionActivity extends XmppActivity
rtpConnection == null ? null : rtpConnection.getAudioManager();
if (audioManager == null
|| audioManager.getSelectedAudioDevice()
== AppRTCAudioManager.AudioDevice.EARPIECE) {
== AppRTCAudioManager.AudioDevice.EARPIECE) {
acquireProximityWakeLock();
}
}
@ -407,7 +420,7 @@ public class RtpSessionActivity extends XmppActivity
}
} else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) {
proposeJingleRtpSession(account, with, actionToMedia(action));
binding.with.setText(account.getRoster().getContact(with).getDisplayName());
setWith(account.getRoster().getContact(with), null);
} else {
throw new IllegalStateException("received onNewIntent without sessionId");
}
@ -420,18 +433,6 @@ public class RtpSessionActivity extends XmppActivity
final Account account = extractAccount(intent);
final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
String accountname;
if (Config.DOMAIN_LOCK != null) {
accountname = account.getJid().getLocal();
} else {
accountname = account.getJid().asBareJid().toEscapedString();
}
binding.detailsAccount.setText(getString(R.string.using_account, accountname));
if (xmppConnectionService.multipleAccounts()) {
binding.detailsAccount.setVisibility(View.VISIBLE);
} else {
binding.detailsAccount.setVisibility(View.GONE);
}
if (sessionId != null) {
if (initializeActivityWithRunningRtpSession(account, with, sessionId)) {
return;
@ -443,7 +444,7 @@ public class RtpSessionActivity extends XmppActivity
}
} else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) {
proposeJingleRtpSession(account, with, actionToMedia(action));
binding.with.setText(account.getRoster().getContact(with).getDisplayName());
setWith(account.getRoster().getContact(with), null);
} else if (Intent.ACTION_VIEW.equals(action)) {
final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
final RtpEndUserState state =
@ -453,10 +454,10 @@ public class RtpSessionActivity extends XmppActivity
updateButtonConfiguration(state);
updateVerifiedShield(false);
updateStateDisplay(state);
updateProfilePicture(state);
updateIncomingCallScreen(state);
invalidateOptionsMenu();
}
binding.with.setText(account.getRoster().getContact(with).getDisplayName());
setWith(account.getRoster().getContact(with), state);
if (xmppConnectionService
.getJingleConnectionManager()
.fireJingleRtpConnectionStateUpdates()) {
@ -464,8 +465,8 @@ public class RtpSessionActivity extends XmppActivity
}
if (END_CARD.contains(state)
|| xmppConnectionService
.getJingleConnectionManager()
.hasMatchingProposal(account, with)) {
.getJingleConnectionManager()
.hasMatchingProposal(account, with)) {
return;
}
Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing");
@ -473,6 +474,21 @@ public class RtpSessionActivity extends XmppActivity
}
}
private void setWidth(final RtpEndUserState state) {
setWith(getWith(), state);
}
private void setWith(final Contact contact, final RtpEndUserState state) {
binding.with.setText(contact.getDisplayName());
if (Arrays.asList(RtpEndUserState.INCOMING_CALL, RtpEndUserState.ACCEPTING_CALL)
.contains(state)) {
binding.withJid.setText(contact.getJid().asBareJid().toEscapedString());
binding.withJid.setVisibility(View.VISIBLE);
} else {
binding.withJid.setVisibility(View.GONE);
}
}
private void proposeJingleRtpSession(
final Account account, final Jid with, final Set<Media> media) {
checkMicrophoneAvailabilityAsync();
@ -515,7 +531,7 @@ public class RtpSessionActivity extends XmppActivity
} else {
throw new IllegalStateException("Invalid permission result request");
}
ToastCompat.makeText(this, getString(res, getString(R.string.app_name)), ToastCompat.LENGTH_SHORT)
Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT)
.show();
}
}
@ -596,17 +612,16 @@ public class RtpSessionActivity extends XmppActivity
return false;
}
@RequiresApi(api = Build.VERSION_CODES.N)
@RequiresApi(api = Build.VERSION_CODES.O)
private void startPictureInPicture() {
try {
if (Compatibility.runsTwentySix()) {
final Rational rational = this.binding.remoteVideo.getAspectRatio();
final Rational clippedRational = Rationals.clip(rational);
Log.d(Config.LOGTAG, "suggested rational " + rational + ". clipped to " + clippedRational);
enterPictureInPictureMode(new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build());
} else {
this.enterPictureInPictureMode();
}
final Rational rational = this.binding.remoteVideo.getAspectRatio();
final Rational clippedRational = Rationals.clip(rational);
Log.d(
Config.LOGTAG,
"suggested rational " + rational + ". clipped to " + clippedRational);
enterPictureInPictureMode(
new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build());
} catch (final IllegalStateException e) {
// this sometimes happens on Samsung phones (possibly when Knox is enabled)
Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e);
@ -641,10 +656,10 @@ public class RtpSessionActivity extends XmppActivity
final JingleRtpConnection rtpConnection = requireRtpConnection();
return rtpConnection.getMedia().contains(Media.VIDEO)
&& Arrays.asList(
RtpEndUserState.ACCEPTING_CALL,
RtpEndUserState.CONNECTING,
RtpEndUserState.CONNECTED)
.contains(rtpConnection.getEndUserState());
RtpEndUserState.ACCEPTING_CALL,
RtpEndUserState.CONNECTING,
RtpEndUserState.CONNECTED)
.contains(rtpConnection.getEndUserState());
} catch (final IllegalStateException e) {
return false;
}
@ -683,12 +698,12 @@ public class RtpSessionActivity extends XmppActivity
requireRtpConnection().getState())) {
putScreenInCallMode();
}
binding.with.setText(getWith().getDisplayName());
setWidth(currentState);
updateVideoViews(currentState);
updateStateDisplay(currentState, media);
updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState));
updateButtonConfiguration(currentState, media);
updateProfilePicture(currentState);
updateIncomingCallScreen(currentState);
invalidateOptionsMenu();
return false;
}
@ -702,15 +717,15 @@ public class RtpSessionActivity extends XmppActivity
finish();
return;
}
RtpEndUserState state = terminatedRtpSession.state;
final RtpEndUserState state = terminatedRtpSession.state;
resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media);
updateButtonConfiguration(state);
updateStateDisplay(state);
updateProfilePicture(state);
updateIncomingCallScreen(state);
updateCallDuration();
updateVerifiedShield(false);
invalidateOptionsMenu();
binding.with.setText(account.getRoster().getContact(with).getDisplayName());
setWith(account.getRoster().getContact(with), state);
}
private void reInitializeActivityWithRunningRtpSession(
@ -807,11 +822,11 @@ public class RtpSessionActivity extends XmppActivity
this.binding.verified.setVisibility(verified ? View.VISIBLE : View.GONE);
}
private void updateProfilePicture(final RtpEndUserState state) {
updateProfilePicture(state, null);
private void updateIncomingCallScreen(final RtpEndUserState state) {
updateIncomingCallScreen(state, null);
}
private void updateProfilePicture(final RtpEndUserState state, final Contact contact) {
private void updateIncomingCallScreen(final RtpEndUserState state, final Contact contact) {
if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) {
final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call);
if (show) {
@ -826,7 +841,14 @@ public class RtpSessionActivity extends XmppActivity
} else {
binding.contactPhoto.setVisibility(View.GONE);
}
final Account account = contact == null ? getWith().getAccount() : contact.getAccount();
binding.detailsAccount.setVisibility(View.VISIBLE); //TODO: Change detailsAccount to usingAccount
binding.detailsAccount.setText( //TODO: Change detailsAccount to usingAccount
getString(
R.string.using_account,
account.getJid().asBareJid().toEscapedString()));
} else {
binding.detailsAccount.setVisibility(View.GONE); //TODO: Change detailsAccount to usingAccount
binding.contactPhoto.setVisibility(View.GONE);
}
}
@ -866,11 +888,11 @@ public class RtpSessionActivity extends XmppActivity
this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_white_24dp);
this.binding.acceptCall.setVisibility(View.VISIBLE);
} else if (asList(
RtpEndUserState.CONNECTIVITY_ERROR,
RtpEndUserState.CONNECTIVITY_LOST_ERROR,
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.RETRACTED,
RtpEndUserState.SECURITY_ERROR)
RtpEndUserState.CONNECTIVITY_ERROR,
RtpEndUserState.CONNECTIVITY_LOST_ERROR,
RtpEndUserState.APPLICATION_ERROR,
RtpEndUserState.RETRACTED,
RtpEndUserState.SECURITY_ERROR)
.contains(state)) {
this.binding.rejectCall.setContentDescription(getString(R.string.exit));
this.binding.rejectCall.setOnClickListener(this::exit);
@ -1008,10 +1030,10 @@ public class RtpSessionActivity extends XmppActivity
Config.LOGTAG,
"could not switch camera",
Throwables.getRootCause(throwable));
ToastCompat.makeText(
Toast.makeText(
RtpSessionActivity.this,
R.string.could_not_switch_camera,
ToastCompat.LENGTH_LONG)
Toast.LENGTH_LONG)
.show();
}
},
@ -1022,7 +1044,7 @@ public class RtpSessionActivity extends XmppActivity
try {
requireRtpConnection().setVideoEnabled(true);
} catch (final IllegalStateException e) {
ToastCompat.makeText(this, R.string.unable_to_enable_video, ToastCompat.LENGTH_SHORT).show();
Toast.makeText(this, R.string.unable_to_enable_video, Toast.LENGTH_SHORT).show();
return;
}
updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
@ -1270,7 +1292,7 @@ public class RtpSessionActivity extends XmppActivity
verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state));
updateButtonConfiguration(state, media);
updateVideoViews(state);
updateProfilePicture(state, contact);
updateIncomingCallScreen(state, contact);
invalidateOptionsMenu();
});
if (END_CARD.contains(state)) {
@ -1331,7 +1353,7 @@ public class RtpSessionActivity extends XmppActivity
updateVerifiedShield(false);
updateStateDisplay(state);
updateButtonConfiguration(state);
updateProfilePicture(state);
updateIncomingCallScreen(state);
invalidateOptionsMenu();
});
resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));

View file

@ -85,4 +85,4 @@ public final class ContentAddition {
.toString();
}
}
}
}

View file

@ -5,16 +5,16 @@ import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.google.common.base.Joiner;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicates;
import com.google.common.base.Stopwatch;
import com.google.common.base.Strings;
import com.google.common.base.Throwables;
import com.google.common.collect.Collections2;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Sets;
import com.google.common.primitives.Ints;
import com.google.common.util.concurrent.FutureCallback;
@ -39,6 +39,7 @@ import java.util.concurrent.ExecutionException;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import eu.siacs.conversations.BuildConfig;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
@ -53,6 +54,7 @@ import eu.siacs.conversations.utils.IP;
import eu.siacs.conversations.xml.Namespace;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
import eu.siacs.conversations.xmpp.jingle.stanzas.Group;
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
@ -163,9 +165,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
private Set<Media> proposedMedia;
private RtpContentMap initiatorRtpContentMap;
private RtpContentMap responderRtpContentMap;
private IceUdpTransportInfo.Setup peerDtlsSetup;
private RtpContentMap incomingContentAdd;
private RtpContentMap outgoingContentAdd;
private IceUdpTransportInfo.Setup peerDtlsSetup;
private final Stopwatch sessionDuration = Stopwatch.createUnstarted();
private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
private ScheduledFuture<?> ringingTimeoutFuture;
@ -229,6 +231,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
case CONTENT_REJECT:
receiveContentReject(jinglePacket);
break;
case CONTENT_REMOVE:
receiveContentRemove(jinglePacket);
break;
default:
respondOk(jinglePacket);
Log.d(
@ -357,10 +362,99 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
private void rejectContent(final RtpContentMap contentMap) {
final JinglePacket jinglePacket =
contentMap
.toStub()
.toJinglePacket(JinglePacket.Action.CONTENT_REJECT, id.sessionId);
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": rejecting content "
+ ContentAddition.summary(contentMap));
send(jinglePacket);
}
private void receiveContentAdd(final JinglePacket jinglePacket) {
final RtpContentMap modification;
try {
modification = RtpContentMap.of(jinglePacket);
modification.requireContentDescriptions();
} catch (final RuntimeException e) {
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid() + ": improperly formatted contents",
Throwables.getRootCause(e));
respondOk(jinglePacket);
webRTCWrapper.close();
sendSessionTerminate(Reason.of(e), e.getMessage());
return;
}
if (isInState(State.SESSION_ACCEPTED)) {
receiveContentAdd(jinglePacket, modification);
} else {
terminateWithOutOfOrder(jinglePacket);
}
}
private void receiveContentAdd(
final JinglePacket jinglePacket, final RtpContentMap modification) {
final RtpContentMap remote = getRemoteContentMap();
if (!Collections.disjoint(modification.getNames(), remote.getNames())) {
respondOk(jinglePacket);
this.webRTCWrapper.close();
sendSessionTerminate(
Reason.FAILED_APPLICATION,
String.format(
"contents with names %s already exists",
Joiner.on(", ").join(modification.getNames())));
return;
}
final ContentAddition contentAddition =
ContentAddition.of(ContentAddition.Direction.INCOMING, modification);
final RtpContentMap outgoing = this.outgoingContentAdd;
final Set<ContentAddition.Summary> outgoingContentAddSummary =
outgoing == null ? Collections.emptySet() : ContentAddition.summary(outgoing);
if (outgoingContentAddSummary.equals(contentAddition.summary)) {
if (isInitiator()) {
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": respond with tie break to matching content-add offer");
respondWithTieBreak(jinglePacket);
} else {
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": automatically accept matching content-add offer");
acceptContentAdd(contentAddition.summary, modification);
}
return;
}
// once we can display multiple video tracks we can be more loose with this condition
// theoretically it should also be fine to automatically accept audio only contents
if (Media.audioOnly(remote.getMedia()) && Media.videoOnly(contentAddition.media())) {
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid() + ": received " + contentAddition);
this.incomingContentAdd = modification;
respondOk(jinglePacket);
updateEndUserState();
} else {
respondOk(jinglePacket);
// TODO do we want to add a reason?
rejectContentAdd(modification);
}
}
private void receiveContentAccept(final JinglePacket jinglePacket) {
final RtpContentMap receivedContentAccept;
try {
receivedContentAccept = RtpContentMap.of(jinglePacket);
receivedContentAccept.requireContentDescriptions();
} catch (final RuntimeException e) {
Log.d(
Config.LOGTAG,
@ -372,55 +466,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
return;
}
// TODO check that it is not adding contents we already have
// reject with some reason
// TODO check for tie-break
// TODO reject if this takes us above one audio and one video track
respondOk(jinglePacket);
// once we can display multiple video tracks we can be more loose with this condition
// theoretically it should also be fine to automatically accept audio only contents
final ContentAddition contentAddition =
ContentAddition.of(ContentAddition.Direction.INCOMING, modification);
if (Media.audioOnly(getRemoteContentMap().getMedia())
&& Media.videoOnly(contentAddition.media())) {
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid() + ": received " + contentAddition);
this.incomingContentAdd = modification;
updateEndUserState();
} else {
rejectContentAdd();
}
}
private void receiveContentAccept(final JinglePacket jinglePacket) {
final RtpContentMap outgoingContentAdd = this.outgoingContentAdd;
final Set<Media> proposedMedia =
outgoingContentAdd == null ? Collections.emptySet() : outgoingContentAdd.getMedia();
final Set<String> proposedContentIds =
outgoingContentAdd == null
? Collections.emptySet()
: outgoingContentAdd.contents.keySet();
final RtpContentMap receivedContentAccept;
try {
receivedContentAccept = RtpContentMap.of(jinglePacket);
} catch (final RuntimeException e) {
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid() + ": improperly formatted contents",
Throwables.getRootCause(e));
respondOk(jinglePacket);
sendSessionTerminate(Reason.of(e), e.getMessage());
if (outgoingContentAdd == null) {
Log.d(Config.LOGTAG, "received content-accept when we had no outgoing content add");
terminateWithOutOfOrder(jinglePacket);
return;
}
if (proposedMedia.containsAll(receivedContentAccept.getMedia())
&& proposedContentIds.containsAll(receivedContentAccept.contents.keySet())) {
final Set<ContentAddition.Summary> ourSummary = ContentAddition.summary(outgoingContentAdd);
if (ourSummary.equals(ContentAddition.summary(receivedContentAccept))) {
this.outgoingContentAdd = null;
respondOk(jinglePacket);
receiveContentAccept(receivedContentAccept);
@ -454,8 +507,14 @@ public class JingleRtpConnection extends AbstractJingleConnection
cause);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
return;
}
Log.d(Config.LOGTAG, "received and processed content-accept");
updateEndUserState();
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": remote has accepted content-add "
+ ContentAddition.summary(receivedContentAccept));
}
private void receiveContentReject(final JinglePacket jinglePacket) {
@ -468,6 +527,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
id.getAccount().getJid().asBareJid() + ": improperly formatted contents",
Throwables.getRootCause(e));
respondOk(jinglePacket);
this.webRTCWrapper.close();
sendSessionTerminate(Reason.of(e), e.getMessage());
return;
}
@ -482,6 +542,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
if (ourSummary.equals(ContentAddition.summary(receivedContentReject))) {
this.outgoingContentAdd = null;
respondOk(jinglePacket);
Log.d(Config.LOGTAG,jinglePacket.toString());
receiveContentReject(ourSummary);
} else {
Log.d(Config.LOGTAG, "received content-reject did not match our outgoing content-add");
@ -489,15 +550,11 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
private void receiveContentReject(final Set<ContentAddition.Summary> contentAddition) {
Log.d(Config.LOGTAG,id.getAccount().getJid().asBareJid()+": received content-reject "+contentAddition);
private void receiveContentReject(final Set<ContentAddition.Summary> summary) {
try {
// TODO specify if we want to remove receivers as well; we can also remove transceivers
// by mid
this.webRTCWrapper.removeTrack(Media.VIDEO);
final SessionDescription sessionDescription = rollbackLocalSessionDescription();
setLocalContentMap(RtpContentMap.of(sessionDescription, isInitiator()));
Log.d(Config.LOGTAG, "rollback complete");
final RtpContentMap localContentMap = customRollback();
modifyLocalContentMap(localContentMap);
} catch (final Exception e) {
final Throwable cause = Throwables.getRootCause(e);
Log.d(
@ -507,7 +564,118 @@ public class JingleRtpConnection extends AbstractJingleConnection
cause);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
return;
}
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": remote has rejected our content-add "
+ summary);
}
private void receiveContentRemove(final JinglePacket jinglePacket) {
final RtpContentMap receivedContentRemove;
try {
receivedContentRemove = RtpContentMap.of(jinglePacket);
receivedContentRemove.requireContentDescriptions();
} catch (final RuntimeException e) {
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid() + ": improperly formatted contents",
Throwables.getRootCause(e));
respondOk(jinglePacket);
this.webRTCWrapper.close();
sendSessionTerminate(Reason.of(e), e.getMessage());
return;
}
respondOk(jinglePacket);
receiveContentRemove(receivedContentRemove);
}
private void receiveContentRemove(final RtpContentMap receivedContentRemove) {
final RtpContentMap incomingContentAdd = this.incomingContentAdd;
final Set<ContentAddition.Summary> contentAddSummary =
incomingContentAdd == null
? Collections.emptySet()
: ContentAddition.summary(incomingContentAdd);
final Set<ContentAddition.Summary> removeSummary =
ContentAddition.summary(receivedContentRemove);
if (contentAddSummary.equals(removeSummary)) {
this.incomingContentAdd = null;
updateEndUserState();
} else {
webRTCWrapper.close();
sendSessionTerminate(
Reason.FAILED_APPLICATION,
String.format(
"%s only supports %s as a means to retract a not yet accepted %s",
BuildConfig.LOGTAG,
JinglePacket.Action.CONTENT_REMOVE,
JinglePacket.Action.CONTENT_ACCEPT));
}
}
public synchronized void retractContentAdd() {
final RtpContentMap outgoingContentAdd = this.outgoingContentAdd;
if (outgoingContentAdd == null) {
throw new IllegalStateException("Not outgoing content add");
}
try {
webRTCWrapper.removeTrack(Media.VIDEO);
final RtpContentMap localContentMap = customRollback();
modifyLocalContentMap(localContentMap);
} catch (final Exception e) {
final Throwable cause = Throwables.getRootCause(e);
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": unable to rollback local description after trying to retract content-add",
cause);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
return;
}
this.outgoingContentAdd = null;
final JinglePacket retract =
outgoingContentAdd
.toStub()
.toJinglePacket(JinglePacket.Action.CONTENT_REMOVE, id.sessionId);
this.send(retract);
Log.d(
Config.LOGTAG,
id.getAccount().getJid()
+ ": retract content-add "
+ ContentAddition.summary(outgoingContentAdd));
}
private RtpContentMap customRollback() throws ExecutionException, InterruptedException {
final SessionDescription sdp = setLocalSessionDescription();
final RtpContentMap localRtpContentMap = RtpContentMap.of(sdp, isInitiator());
final SessionDescription answer = generateFakeResponse(localRtpContentMap);
this.webRTCWrapper
.setRemoteDescription(
new org.webrtc.SessionDescription(
org.webrtc.SessionDescription.Type.ANSWER, answer.toString()))
.get();
return localRtpContentMap;
}
private SessionDescription generateFakeResponse(final RtpContentMap localContentMap) {
final RtpContentMap currentRemote = getRemoteContentMap();
final RtpContentMap.Diff diff = currentRemote.diff(localContentMap);
if (diff.isEmpty()) {
throw new IllegalStateException(
"Unexpected rollback condition. No difference between local and remote");
}
final RtpContentMap patch = localContentMap.toContentModification(diff.added);
if (ImmutableSet.of(Content.Senders.NONE).equals(patch.getSenders())) {
final RtpContentMap nextRemote =
currentRemote.addContent(
patch.modifiedSenders(Content.Senders.NONE), getPeerDtlsSetup());
return SessionDescription.of(nextRemote, !isInitiator());
}
throw new IllegalStateException(
"Unexpected rollback condition. Senders were not uniformly none");
}
public synchronized void acceptContentAdd(@NonNull final Set<ContentAddition.Summary> contentAddition) {
@ -533,7 +701,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
try {
offer = SessionDescription.of(modifiedContentMap, !isInitiator());
} catch (final IllegalArgumentException | NullPointerException e) {
Log.d(Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": unable convert offer from session-initiate to SDP", e);
Log.d(Config.LOGTAG, id.getAccount().getJid().asBareJid() + ": unable convert offer from content-add to SDP", e);
webRTCWrapper.close();
sendSessionTerminate(Reason.FAILED_APPLICATION, e.getMessage());
return;
@ -542,32 +710,34 @@ public class JingleRtpConnection extends AbstractJingleConnection
acceptContentAdd(contentAddition, offer);
}
private void acceptContentAdd(final Set<ContentAddition.Summary> contentAddition, final SessionDescription offer) {
final org.webrtc.SessionDescription sdp = new org.webrtc.SessionDescription(
org.webrtc.SessionDescription.Type.OFFER,
offer.toString()
);
private void acceptContentAdd(
final Set<ContentAddition.Summary> contentAddition, final SessionDescription offer) {
final org.webrtc.SessionDescription sdp =
new org.webrtc.SessionDescription(
org.webrtc.SessionDescription.Type.OFFER, offer.toString());
try {
this.webRTCWrapper.setRemoteDescription(sdp).get();
// TODO add tracks for 'media' where contentAddition.senders matches
//TODO if senders.sending(isInitiator())
// TODO if senders.sending(isInitiator())
this.webRTCWrapper.addTrack(Media.VIDEO);
//TODO add additional transceivers for recv only cases
// TODO add additional transceivers for recv only cases
final SessionDescription answer = setLocalSessionDescription();
final RtpContentMap rtpContentMap = RtpContentMap.of(answer, isInitiator());
final RtpContentMap.Diff diff = getLocalContentMap().diff(rtpContentMap);
final RtpContentMap contentAcceptMap = rtpContentMap.toContentModification(diff.added);
setLocalContentMap(rtpContentMap);
// TODO is this a good time to update the UI? once we've fixed getMedia() this will trigger the buttons to be re-arranged
updateEndUserState();
Log.d(Config.LOGTAG,"accept content add with "+diff);
final RtpContentMap contentAcceptMap =
rtpContentMap.toContentModification(
Collections2.transform(contentAddition, ca -> ca.name));
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": sending content-accept "
+ ContentAddition.summary(contentAcceptMap));
modifyLocalContentMap(rtpContentMap);
sendContentAccept(contentAcceptMap);
} catch (final Exception e) {
Log.d(Config.LOGTAG, "unable to accept content add", Throwables.getRootCause(e));
@ -576,9 +746,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
private void sendContentAccept(final RtpContentMap contentAcceptMap) {
Log.d(Config.LOGTAG,"sending content-accept");
final JinglePacket jinglePacket = contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId);
send(jinglePacket);
}
@ -590,10 +758,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
this.incomingContentAdd = null;
updateEndUserState();
rejectContent(incomingContentAdd);
rejectContentAdd(incomingContentAdd);
}
private void rejectContent(final RtpContentMap contentMap) {
private void rejectContentAdd(final RtpContentMap contentMap) {
final JinglePacket jinglePacket =
contentMap
.toStub()
@ -606,6 +774,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
send(jinglePacket);
}
private boolean checkForIceRestart(
final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) {
final RtpContentMap existing = getRemoteContentMap();
@ -1868,6 +2037,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
}
}
public Set<Media> getMedia() {
final State current = getState();
if (current == State.NULL) {
@ -1881,14 +2051,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
return Preconditions.checkNotNull(
this.proposedMedia, "RTP connection has not been initialized properly");
}
final RtpContentMap localContentMap = getLocalContentMap();
final RtpContentMap initiatorContentMap = initiatorRtpContentMap;
if (initiatorContentMap != null) {
if (localContentMap != null) {
return localContentMap.getMedia();
} else if (initiatorContentMap != null) {
return initiatorContentMap.getMedia();
} else if (isTerminated()) {
return Collections.emptySet(); // we might fail before we ever got a chance to set media
return Collections.emptySet(); //we might fail before we ever got a chance to set media
} else {
return Preconditions.checkNotNull(
this.proposedMedia, "RTP connection has not been initialized properly");
return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly");
}
}
@ -1912,6 +2084,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
return webRTCWrapper.addTrack(media);
}
public synchronized void acceptCall() {
switch (this.state) {
case PROPOSED:
@ -2030,17 +2204,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
finish();
}
private void setupWebRTC(
final Set<Media> media, final List<PeerConnection.IceServer> iceServers)
throws WebRTCWrapper.InitializationException {
private void setupWebRTC(final Set<Media> media, final List<PeerConnection.IceServer> iceServers) throws WebRTCWrapper.InitializationException {
this.jingleConnectionManager.ensureConnectionIsRegistered(this);
final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference;
if (media.contains(Media.VIDEO)) {
speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.SPEAKER;
} else {
speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.EARPIECE;
}
this.webRTCWrapper.setup(this.xmppConnectionService, speakerPhonePreference);
this.webRTCWrapper.setup(this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media));
this.webRTCWrapper.initializePeerConnection(media, iceServers);
}
@ -2192,11 +2358,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection);
return;
} else {
webRTCWrapper.restartIce();
this.restartIce();
}
}
updateEndUserState();
}
private void restartIce() {
this.stateHistory.clear();
this.webRTCWrapper.restartIce();
}
@Override
public void onRenegotiationNeeded() {
@ -2210,9 +2381,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void renegotiate() {
Log.d(Config.LOGTAG,"method JingleRtpConnection.renegotiate()");
//TODO needs to be called only for ice restarts; maybe in the call to restartICe()
this.stateHistory.clear();
this.webRTCWrapper.setIsReadyToReceiveIceCandidates(false);
final SessionDescription sessionDescription;
try {
sessionDescription = setLocalSessionDescription();
@ -2236,16 +2404,24 @@ public class JingleRtpConnection extends AbstractJingleConnection
+ diff);
if (diff.hasModifications() && iceRestart) {
sendSessionTerminate(Reason.FAILED_APPLICATION, "WebRTC unexpectedly tried to modify content and transport at once");
sendSessionTerminate(
Reason.FAILED_APPLICATION,
"WebRTC unexpectedly tried to modify content and transport at once");
return;
}
if (iceRestart) {
initiateIceRestart(rtpContentMap);
return;
} else if (diff.isEmpty()) {
Log.d(
Config.LOGTAG,
"renegotiation. nothing to do. SignalingState="
+ this.webRTCWrapper.getSignalingState());
}
if (diff.added.size() > 0) {
modifyLocalContentMap(rtpContentMap);
sendContentAdd(rtpContentMap, diff.added);
}
@ -2268,8 +2444,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
return;
}
if (response.getType() == IqPacket.TYPE.ERROR) {
final Element error = response.findChild("error");
if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) {
if (isTieBreak(response)) {
Log.d(Config.LOGTAG, "received tie-break as result of ice restart");
return;
}
@ -2281,14 +2456,39 @@ public class JingleRtpConnection extends AbstractJingleConnection
});
}
private boolean isTieBreak(final IqPacket response) {
final Element error = response.findChild("error");
return error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS);
}
private void sendContentAdd(final RtpContentMap rtpContentMap, final Collection<String> added) {
final RtpContentMap contentAdd = rtpContentMap.toContentModification(added);
// TODO setLocalContentMap and refresh UI?
this.outgoingContentAdd = contentAdd;
final JinglePacket jinglePacket = contentAdd.toJinglePacket(JinglePacket.Action.CONTENT_ADD, id.sessionId);
this.send(jinglePacket);
final JinglePacket jinglePacket =
contentAdd.toJinglePacket(JinglePacket.Action.CONTENT_ADD, id.sessionId);
jinglePacket.setTo(id.with);
xmppConnectionService.sendIqPacket(
id.account,
jinglePacket,
(connection, response) -> {
if (response.getType() == IqPacket.TYPE.RESULT) {
Log.d(
Config.LOGTAG,
id.getAccount().getJid().asBareJid()
+ ": received ACK to our content-add");
return;
}
if (response.getType() == IqPacket.TYPE.ERROR) {
if (isTieBreak(response)) {
this.outgoingContentAdd = null;
Log.d(Config.LOGTAG, "received tie-break as result of our content-add");
return;
}
handleIqErrorResponse(response);
}
if (response.getType() == IqPacket.TYPE.TIMEOUT) {
handleIqTimeoutResponse(response);
}
});
}
@ -2307,6 +2507,15 @@ public class JingleRtpConnection extends AbstractJingleConnection
this.initiatorRtpContentMap = rtpContentMap;
}
}
// this method is to be used for content map modifications that modify media
private void modifyLocalContentMap(final RtpContentMap rtpContentMap) {
final RtpContentMap activeContents = rtpContentMap.activeContents();
setLocalContentMap(activeContents);
this.webRTCWrapper.switchSpeakerPhonePreference(
AppRTCAudioManager.SpeakerPhonePreference.of(activeContents.getMedia()));
updateEndUserState();
}
private SessionDescription setLocalSessionDescription()
throws ExecutionException, InterruptedException {

View file

@ -4,7 +4,6 @@ import com.google.common.collect.ImmutableSet;
import java.util.Locale;
import java.util.Set;
import javax.annotation.Nonnull;
public enum Media {

View file

@ -12,7 +12,7 @@ import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;
import java.util.HashMap;
import java.util.Collection;
import java.util.List;
import java.util.Map;
@ -92,6 +92,10 @@ public class RtpContentMap {
}));
}
public Set<Content.Senders> getSenders() {
return ImmutableSet.copyOf(Collections2.transform(contents.values(),dt -> dt.senders));
}
public List<String> getNames() {
return ImmutableList.copyOf(contents.keySet());
}
@ -280,6 +284,14 @@ public class RtpContentMap {
}
return new RtpContentMap(this.group, contentMapBuilder.build());
}
public RtpContentMap modifiedSenders(final Content.Senders senders) {
return new RtpContentMap(
this.group,
Maps.transformValues(
contents,
dt -> new DescriptionTransport(senders, dt.description, dt.transport)));
}
public RtpContentMap toContentModification(final Collection<String> modifications) {
return new RtpContentMap(
@ -303,6 +315,11 @@ public class RtpContentMap {
IceUdpTransportInfo.STUB)));
}
public RtpContentMap activeContents() {
return new RtpContentMap(group, Maps.filterValues(this.contents, dt -> dt.senders != Content.Senders.NONE));
}
public Diff diff(final RtpContentMap rtpContentMap) {
final Set<String> existingContentIds = this.contents.keySet();
final Set<String> newContentIds = rtpContentMap.contents.keySet();
@ -325,11 +342,11 @@ public class RtpContentMap {
final DTLS dtls = getDistinctDtls();
final IceUdpTransportInfo iceUdpTransportInfo =
IceUdpTransportInfo.of(credentials, setup, dtls.hash, dtls.fingerprint);
final Map<String, DescriptionTransport> combined =
new ImmutableMap.Builder<String, DescriptionTransport>()
final Map<String, DescriptionTransport> combined = merge(contents, modification.contents);
/*new ImmutableMap.Builder<String, DescriptionTransport>()
.putAll(contents)
.putAll(modification.contents)
.build();
.build();*/
final Map<String, DescriptionTransport> combinedFixedTransport =
Maps.transformValues(
combined,
@ -339,6 +356,14 @@ public class RtpContentMap {
return new RtpContentMap(modification.group, combinedFixedTransport);
}
private static Map<String, DescriptionTransport> merge(
final Map<String, DescriptionTransport> a, final Map<String, DescriptionTransport> b) {
final Map<String, DescriptionTransport> combined = new HashMap<>();
combined.putAll(a);
combined.putAll(b);
return ImmutableMap.copyOf(combined);
}
public static class DescriptionTransport {
public final Content.Senders senders;
public final RtpDescription description;
@ -421,6 +446,9 @@ public class RtpContentMap {
public boolean hasModifications() {
return !this.added.isEmpty() || !this.removed.isEmpty();
}
public boolean isEmpty() {
return this.added.isEmpty() && this.removed.isEmpty();
}
@Override
@Nonnull

View file

@ -299,7 +299,7 @@ public class SessionDescription {
mediaAttributes.put("mid", name);
mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), "");
if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP)) {
if (description.hasChild("rtcp-mux", Namespace.JINGLE_APPS_RTP) || group != null) {
mediaAttributes.put("rtcp-mux", "");
}

View file

@ -4,7 +4,7 @@ import android.content.Context;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.util.Log;
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
@ -19,6 +19,7 @@ class ToneManager {
private final Context context;
private ToneState state = null;
private RtpEndUserState endUserState = null;
private ScheduledFuture<?> currentTone;
private ScheduledFuture<?> currentResetFuture;
private boolean appRtcAudioManagerHasControl = false;
@ -51,7 +52,11 @@ class ToneManager {
return ToneState.ENDING_CALL;
}
}
if (state == RtpEndUserState.CONNECTED || state == RtpEndUserState.RECONNECTING) {
if (Arrays.asList(
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING,
RtpEndUserState.INCOMING_CONTENT_ADD)
.contains(state)) {
if (media.contains(Media.VIDEO)) {
return ToneState.NULL;
} else {
@ -62,14 +67,19 @@ class ToneManager {
}
void transition(final RtpEndUserState state, final Set<Media> media) {
transition(of(true, state, media), media);
transition(state, of(true, state, media), media);
}
void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
transition(of(isInitiator, state, media), media);
transition(state, of(isInitiator, state, media), media);
}
private synchronized void transition(ToneState state, final Set<Media> media) {
private synchronized void transition(final RtpEndUserState endUserState, final ToneState state, final Set<Media> media) {
final RtpEndUserState normalizeEndUserState = normalize(endUserState);
if (this.endUserState == normalizeEndUserState) {
return;
}
this.endUserState = normalizeEndUserState;
if (this.state == state) {
return;
}
@ -105,6 +115,19 @@ class ToneManager {
this.state = state;
}
private static RtpEndUserState normalize(final RtpEndUserState endUserState) {
if (Arrays.asList(
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING,
RtpEndUserState.INCOMING_CONTENT_ADD)
.contains(endUserState)) {
return RtpEndUserState.CONNECTED;
} else {
return endUserState;
}
}
void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) {
this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl;
}

View file

@ -2,10 +2,20 @@ package eu.siacs.conversations.xmpp.jingle;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import android.util.Log;
import com.google.common.base.CaseFormat;
import org.webrtc.MediaStreamTrack;
import org.webrtc.PeerConnection;
import org.webrtc.RtpSender;
import org.webrtc.RtpTransceiver;
import java.util.UUID;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import eu.siacs.conversations.Config;
class TrackWrapper<T extends MediaStreamTrack> {
public final T track;
@ -25,7 +35,41 @@ class TrackWrapper<T extends MediaStreamTrack> {
}
public static <T extends MediaStreamTrack> Optional<T> get(
final TrackWrapper<T> trackWrapper) {
return trackWrapper == null ? Optional.absent() : Optional.of(trackWrapper.track);
@Nullable final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) {
if (trackWrapper == null) {
return Optional.absent();
}
final RtpTransceiver transceiver =
peerConnection == null ? null : getTransceiver(peerConnection, trackWrapper);
if (transceiver == null) {
Log.w(Config.LOGTAG, "unable to detect transceiver for " + trackWrapper.rtpSender.id());
return Optional.of(trackWrapper.track);
}
final RtpTransceiver.RtpTransceiverDirection direction = transceiver.getDirection();
if (direction == RtpTransceiver.RtpTransceiverDirection.SEND_ONLY
|| direction == RtpTransceiver.RtpTransceiverDirection.SEND_RECV) {
return Optional.of(trackWrapper.track);
} else {
Log.d(Config.LOGTAG, "withholding track because transceiver is " + direction);
return Optional.absent();
}
}
public static <T extends MediaStreamTrack> RtpTransceiver getTransceiver(
@Nonnull final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) {
final RtpSender rtpSender = trackWrapper.rtpSender;
for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) {
if (transceiver.getSender().id().equals(rtpSender.id())) {
return transceiver;
}
}
return null;
}
public static String id(final Class<? extends MediaStreamTrack> clazz) {
return String.format(
"%s-%s",
CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_HYPHEN, clazz.getSimpleName()),
UUID.randomUUID().toString());
}
}

View file

@ -226,7 +226,7 @@ public class WebRTCWrapper {
public void setup(
final XmppConnectionService service,
final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
@Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
throws InitializationException {
try {
PeerConnectionFactory.initialize(
@ -337,18 +337,31 @@ public class WebRTCWrapper {
}
}
private boolean addAudioTrack(final PeerConnection peerConnection) {
final AudioSource audioSource =
requirePeerConnectionFactory().createAudioSource(new MediaConstraints());
final AudioTrack audioTrack =
requirePeerConnectionFactory().createAudioTrack("my-audio-track", audioSource);
requirePeerConnectionFactory()
.createAudioTrack(TrackWrapper.id(AudioTrack.class), audioSource);
this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack);
return true;
}
private boolean addVideoTrack(final PeerConnection peerConnection) {
Preconditions.checkState(
this.localVideoTrack == null, "A local video track already exists");
final TrackWrapper<VideoTrack> existing = this.localVideoTrack;
if (existing != null) {
final RtpTransceiver transceiver =
TrackWrapper.getTransceiver(peerConnection, existing);
if (transceiver == null) {
Log.w(EXTENDED_LOGGING_TAG, "unable to restart video transceiver");
return false;
}
transceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.SEND_RECV);
this.videoSourceWrapper.startCapture();
return true;
}
final VideoSourceWrapper videoSourceWrapper;
try {
videoSourceWrapper = initializeVideoSourceWrapper();
@ -358,7 +371,9 @@ public class WebRTCWrapper {
}
final VideoTrack videoTrack =
requirePeerConnectionFactory()
.createVideoTrack("my-video-track", videoSourceWrapper.getVideoSource());
.createVideoTrack(
TrackWrapper.id(VideoTrack.class),
videoSourceWrapper.getVideoSource());
// TODO do we want to create Transceiver manually and be able to set direction and keep a
// reference to it for later removal
this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack);
@ -369,19 +384,14 @@ public class WebRTCWrapper {
private void removeVideoTrack(final PeerConnection peerConnection) {
final TrackWrapper<VideoTrack> localVideoTrack = this.localVideoTrack;
if (localVideoTrack != null) {
final boolean success = peerConnection.removeTrack(localVideoTrack.rtpSender);
Log.d(Config.LOGTAG, "removeVideoTrack. success=" + success);
for (final RtpTransceiver transceiver : peerConnection.getTransceivers()) {
if (transceiver.getMediaType() == MediaStreamTrack.MediaType.MEDIA_TYPE_VIDEO) {
final RtpTransceiver.RtpTransceiverDirection direction =
transceiver.getDirection();
transceiver.stop();
Log.d(Config.LOGTAG, "stopped video transceiver for direction " + direction);
}
final RtpTransceiver exactTransceiver =
TrackWrapper.getTransceiver(peerConnection, localVideoTrack);
if (exactTransceiver == null) {
throw new IllegalStateException();
}
exactTransceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.INACTIVE);
}
this.localVideoTrack = null;
this.eventCallback.onTrackModification();
final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
if (videoSourceWrapper != null) {
try {
@ -392,6 +402,8 @@ public class WebRTCWrapper {
}
}
private static PeerConnection.RTCConfiguration buildConfiguration(
final List<PeerConnection.IceServer> iceServers) {
final PeerConnection.RTCConfiguration rtcConfig =
@ -411,7 +423,12 @@ public class WebRTCWrapper {
}
void restartIce() {
executorService.execute(() -> requirePeerConnection().restartIce());
executorService.execute(() -> {
final PeerConnection peerConnection = requirePeerConnection();
setIsReadyToReceiveIceCandidates(false);
peerConnection.restartIce();
requirePeerConnection().restartIce();}
);
}
public void setIsReadyToReceiveIceCandidates(final boolean ready) {
@ -486,7 +503,8 @@ public class WebRTCWrapper {
}
boolean isMicrophoneEnabled() {
final Optional<AudioTrack> audioTrack = TrackWrapper.get(this.localAudioTrack);
final Optional<AudioTrack> audioTrack =
TrackWrapper.get(peerConnection, this.localAudioTrack);
if (audioTrack.isPresent()) {
try {
return audioTrack.get().enabled();
@ -501,7 +519,8 @@ public class WebRTCWrapper {
}
boolean setMicrophoneEnabled(final boolean enabled) {
final Optional<AudioTrack> audioTrack = TrackWrapper.get(this.localAudioTrack);
final Optional<AudioTrack> audioTrack =
TrackWrapper.get(peerConnection, this.localAudioTrack);
if (audioTrack.isPresent()) {
try {
audioTrack.get().setEnabled(enabled);
@ -517,7 +536,8 @@ public class WebRTCWrapper {
}
boolean isVideoEnabled() {
final Optional<VideoTrack> videoTrack = TrackWrapper.get(this.localVideoTrack);
final Optional<VideoTrack> videoTrack =
TrackWrapper.get(peerConnection, this.localVideoTrack);
if (videoTrack.isPresent()) {
return videoTrack.get().enabled();
}
@ -525,7 +545,8 @@ public class WebRTCWrapper {
}
void setVideoEnabled(final boolean enabled) {
final Optional<VideoTrack> videoTrack = TrackWrapper.get(this.localVideoTrack);
final Optional<VideoTrack> videoTrack =
TrackWrapper.get(peerConnection, this.localVideoTrack);
if (videoTrack.isPresent()) {
videoTrack.get().setEnabled(enabled);
return;
@ -596,7 +617,7 @@ public class WebRTCWrapper {
MoreExecutors.directExecutor());
}
private static void logDescription(final SessionDescription sessionDescription) {
public static void logDescription(final SessionDescription sessionDescription) {
for (final String line :
sessionDescription.description.split(
eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
@ -680,7 +701,7 @@ public class WebRTCWrapper {
}
Optional<VideoTrack> getLocalVideoTrack() {
return TrackWrapper.get(this.localVideoTrack);
return TrackWrapper.get(peerConnection, this.localVideoTrack);
}
Optional<VideoTrack> getRemoteVideoTrack() {
@ -703,6 +724,11 @@ public class WebRTCWrapper {
executorService.execute(command);
}
public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) {
mainHandler.post(() -> appRTCAudioManager.switchSpeakerPhonePreference(preference));
}
public interface EventCallback {
void onIceCandidate(IceCandidate iceCandidate);

View file

@ -61,7 +61,8 @@ public class RtpDescription extends GenericDescription {
public List<RtpHeaderExtension> getHeaderExtensions() {
final ImmutableList.Builder<RtpHeaderExtension> builder = new ImmutableList.Builder<>();
for (final Element child : getChildren()) {
if ("rtp-hdrext".equals(child.getName()) && Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) {
if ("rtp-hdrext".equals(child.getName())
&& Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(child.getNamespace())) {
builder.add(RtpHeaderExtension.upgrade(child));
}
}
@ -71,7 +72,9 @@ public class RtpDescription extends GenericDescription {
public List<Source> getSources() {
final ImmutableList.Builder<Source> builder = new ImmutableList.Builder<>();
for (final Element child : this.children) {
if ("source".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) {
if ("source".equals(child.getName())
&& Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(
child.getNamespace())) {
builder.add(Source.upgrade(child));
}
}
@ -81,7 +84,9 @@ public class RtpDescription extends GenericDescription {
public List<SourceGroup> getSourceGroups() {
final ImmutableList.Builder<SourceGroup> builder = new ImmutableList.Builder<>();
for (final Element child : this.children) {
if ("ssrc-group".equals(child.getName()) && Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(child.getNamespace())) {
if ("ssrc-group".equals(child.getName())
&& Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(
child.getNamespace())) {
builder.add(SourceGroup.upgrade(child));
}
}
@ -89,8 +94,12 @@ public class RtpDescription extends GenericDescription {
}
public static RtpDescription upgrade(final Element element) {
Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description");
Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace");
Preconditions.checkArgument(
"description".equals(element.getName()),
"Name of provided element is not description");
Preconditions.checkArgument(
Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()),
"Element does not match the jingle rtp namespace");
final RtpDescription description = new RtpDescription();
description.setAttributes(element.getAttributes());
description.setChildren(element.getChildren());
@ -120,7 +129,8 @@ public class RtpDescription extends GenericDescription {
private static FeedbackNegotiation upgrade(final Element element) {
Preconditions.checkArgument("rtcp-fb".equals(element.getName()));
Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
Preconditions.checkArgument(
Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
final FeedbackNegotiation feedback = new FeedbackNegotiation();
feedback.setAttributes(element.getAttributes());
feedback.setChildren(element.getChildren());
@ -130,13 +140,13 @@ public class RtpDescription extends GenericDescription {
public static List<FeedbackNegotiation> fromChildren(final List<Element> children) {
ImmutableList.Builder<FeedbackNegotiation> builder = new ImmutableList.Builder<>();
for (final Element child : children) {
if ("rtcp-fb".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
if ("rtcp-fb".equals(child.getName())
&& Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
builder.add(upgrade(child));
}
}
return builder.build();
}
}
public static class FeedbackNegotiationTrrInt extends Element {
@ -146,7 +156,6 @@ public class RtpDescription extends GenericDescription {
this.setAttribute("value", value);
}
private FeedbackNegotiationTrrInt() {
super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
}
@ -154,12 +163,12 @@ public class RtpDescription extends GenericDescription {
public int getValue() {
final String value = getAttribute("value");
return Integer.parseInt(value);
}
private static FeedbackNegotiationTrrInt upgrade(final Element element) {
Preconditions.checkArgument("rtcp-fb-trr-int".equals(element.getName()));
Preconditions.checkArgument(Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
Preconditions.checkArgument(
Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(element.getNamespace()));
final FeedbackNegotiationTrrInt trr = new FeedbackNegotiationTrrInt();
trr.setAttributes(element.getAttributes());
trr.setChildren(element.getChildren());
@ -167,9 +176,11 @@ public class RtpDescription extends GenericDescription {
}
public static List<FeedbackNegotiationTrrInt> fromChildren(final List<Element> children) {
ImmutableList.Builder<FeedbackNegotiationTrrInt> builder = new ImmutableList.Builder<>();
ImmutableList.Builder<FeedbackNegotiationTrrInt> builder =
new ImmutableList.Builder<>();
for (final Element child : children) {
if ("rtcp-fb-trr-int".equals(child.getName()) && Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
if ("rtcp-fb-trr-int".equals(child.getName())
&& Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION.equals(child.getNamespace())) {
builder.add(upgrade(child));
}
}
@ -178,8 +189,8 @@ public class RtpDescription extends GenericDescription {
}
//XEP-0294: Jingle RTP Header Extensions Negotiation
//maps to `extmap:$id $uri`
// XEP-0294: Jingle RTP Header Extensions Negotiation
// maps to `extmap:$id $uri`
public static class RtpHeaderExtension extends Element {
private RtpHeaderExtension() {
@ -202,7 +213,8 @@ public class RtpDescription extends GenericDescription {
public static RtpHeaderExtension upgrade(final Element element) {
Preconditions.checkArgument("rtp-hdrext".equals(element.getName()));
Preconditions.checkArgument(Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace()));
Preconditions.checkArgument(
Namespace.JINGLE_RTP_HEADER_EXTENSIONS.equals(element.getNamespace()));
final RtpHeaderExtension extension = new RtpHeaderExtension();
extension.setAttributes(element.getAttributes());
extension.setChildren(element.getChildren());
@ -221,7 +233,7 @@ public class RtpDescription extends GenericDescription {
}
}
//maps to `rtpmap:$id $name/$clockrate/$channels`
// maps to `rtpmap:$id $name/$clockrate/$channels`
public static class PayloadType extends Element {
private PayloadType() {
@ -242,8 +254,14 @@ public class RtpDescription extends GenericDescription {
final int channels = getChannels();
final String name = getPayloadTypeName();
Preconditions.checkArgument(name != null, "Payload-type name must not be empty");
SessionDescription.checkNoWhitespace(name, "payload-type name must not contain whitespaces");
return getId() + " " + name + "/" + getClockRate() + (channels == 1 ? "" : "/" + channels);
SessionDescription.checkNoWhitespace(
name, "payload-type name must not contain whitespaces");
return getId()
+ " "
+ name
+ "/"
+ getClockRate()
+ (channels == 1 ? "" : "/" + channels);
}
public int getIntId() {
@ -255,7 +273,6 @@ public class RtpDescription extends GenericDescription {
return this.getAttribute("id");
}
public String getPayloadTypeName() {
return this.getAttribute("name");
}
@ -275,7 +292,8 @@ public class RtpDescription extends GenericDescription {
public int getChannels() {
final String channels = this.getAttribute("channels");
if (channels == null) {
return 1; // The number of channels; if omitted, it MUST be assumed to contain one channel
return 1; // The number of channels; if omitted, it MUST be assumed to contain one
// channel
}
try {
return Integer.parseInt(channels);
@ -303,7 +321,9 @@ public class RtpDescription extends GenericDescription {
}
public static PayloadType of(final Element element) {
Preconditions.checkArgument("payload-type".equals(element.getName()), "element name must be called payload-type");
Preconditions.checkArgument(
"payload-type".equals(element.getName()),
"element name must be called payload-type");
PayloadType payloadType = new PayloadType();
payloadType.setAttributes(element.getAttributes());
payloadType.setChildren(element.getChildren());
@ -343,8 +363,8 @@ public class RtpDescription extends GenericDescription {
}
}
//map to `fmtp $id key=value;key=value
//where id is the id of the parent payload-type
// map to `fmtp $id key=value;key=value
// where id is the id of the parent payload-type
public static class Parameter extends Element {
private Parameter() {
@ -366,7 +386,8 @@ public class RtpDescription extends GenericDescription {
}
public static Parameter of(final Element element) {
Preconditions.checkArgument("parameter".equals(element.getName()), "element name must be called parameter");
Preconditions.checkArgument(
"parameter".equals(element.getName()), "element name must be called parameter");
Parameter parameter = new Parameter();
parameter.setAttributes(element.getAttributes());
parameter.setChildren(element.getChildren());
@ -379,12 +400,18 @@ public class RtpDescription extends GenericDescription {
for (int i = 0; i < parameters.size(); ++i) {
final Parameter p = parameters.get(i);
final String name = p.getParameterName();
Preconditions.checkArgument(name != null, String.format("parameter for %s must have a name", id));
SessionDescription.checkNoWhitespace(name, String.format("parameter names for %s must not contain whitespaces", id));
Preconditions.checkArgument(
name != null, String.format("parameter for %s must have a name", id));
SessionDescription.checkNoWhitespace(
name,
String.format("parameter names for %s must not contain whitespaces", id));
final String value = p.getParameterValue();
Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id));
SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id));
Preconditions.checkArgument(
value != null, String.format("parameter for %s must have a value", id));
SessionDescription.checkNoWhitespace(
value,
String.format("parameter values for %s must not contain whitespaces", id));
stringBuilder.append(name).append('=').append(value);
if (i != parameters.size() - 1) {
@ -397,8 +424,11 @@ public class RtpDescription extends GenericDescription {
public static String toSdpString(final String id, final Parameter parameter) {
final String name = parameter.getParameterName();
final String value = parameter.getParameterValue();
Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id));
SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id));
Preconditions.checkArgument(
value != null, String.format("parameter for %s must have a value", id));
SessionDescription.checkNoWhitespace(
value,
String.format("parameter values for %s must not contain whitespaces", id));
if (Strings.isNullOrEmpty(name)) {
return String.format("%s %s", id, value);
} else {
@ -424,8 +454,8 @@ public class RtpDescription extends GenericDescription {
}
}
//XEP-0339: Source-Specific Media Attributes in Jingle
//maps to `a=ssrc:<ssrc-id> <attribute>:<value>`
// XEP-0339: Source-Specific Media Attributes in Jingle
// maps to `a=ssrc:<ssrc-id> <attribute>:<value>`
public static class Source extends Element {
private Source() {
@ -456,7 +486,9 @@ public class RtpDescription extends GenericDescription {
public static Source upgrade(final Element element) {
Preconditions.checkArgument("source".equals(element.getName()));
Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace()));
Preconditions.checkArgument(
Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(
element.getNamespace()));
final Source source = new Source();
source.setChildren(element.getChildren());
source.setAttributes(element.getAttributes());
@ -529,7 +561,9 @@ public class RtpDescription extends GenericDescription {
public static SourceGroup upgrade(final Element element) {
Preconditions.checkArgument("ssrc-group".equals(element.getName()));
Preconditions.checkArgument(Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(element.getNamespace()));
Preconditions.checkArgument(
Namespace.JINGLE_RTP_SOURCE_SPECIFIC_MEDIA_ATTRIBUTES.equals(
element.getNamespace()));
final SourceGroup group = new SourceGroup();
group.setChildren(element.getChildren());
group.setAttributes(element.getAttributes());
@ -537,15 +571,18 @@ public class RtpDescription extends GenericDescription {
}
}
public static RtpDescription of(final SessionDescription sessionDescription, final SessionDescription.Media media) {
public static RtpDescription of(
final SessionDescription sessionDescription, final SessionDescription.Media media) {
final RtpDescription rtpDescription = new RtpDescription(media.media);
final Map<String, List<Parameter>> parameterMap = new HashMap<>();
final ArrayListMultimap<String, Element> feedbackNegotiationMap = ArrayListMultimap.create();
final ArrayListMultimap<String, Source.Parameter> sourceParameterMap = ArrayListMultimap.create();
final Set<String> attributes = Sets.newHashSet(Iterables.concat(
sessionDescription.attributes.keySet(),
media.attributes.keySet()
));
final ArrayListMultimap<String, Element> feedbackNegotiationMap =
ArrayListMultimap.create();
final ArrayListMultimap<String, Source.Parameter> sourceParameterMap =
ArrayListMultimap.create();
final Set<String> attributes =
Sets.newHashSet(
Iterables.concat(
sessionDescription.attributes.keySet(), media.attributes.keySet()));
for (final String rtcpFb : media.attributes.get("rtcp-fb")) {
final String[] parts = rtcpFb.split(" ");
if (parts.length >= 2) {
@ -554,7 +591,10 @@ public class RtpDescription extends GenericDescription {
final String subType = parts.length >= 3 ? parts[2] : null;
if ("trr-int".equals(type)) {
if (subType != null) {
feedbackNegotiationMap.put(id, new FeedbackNegotiationTrrInt(SessionDescription.ignorantIntParser(subType)));
feedbackNegotiationMap.put(
id,
new FeedbackNegotiationTrrInt(
SessionDescription.ignorantIntParser(subType)));
}
} else {
feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType));
@ -606,7 +646,8 @@ public class RtpDescription extends GenericDescription {
rtpDescription.addChild(new SourceGroup(semantics, builder.build()));
}
}
for (Map.Entry<String, Collection<Source.Parameter>> source : sourceParameterMap.asMap().entrySet()) {
for (Map.Entry<String, Collection<Source.Parameter>> source :
sourceParameterMap.asMap().entrySet()) {
rtpDescription.addChild(new Source(source.getKey(), source.getValue()));
}
if (media.attributes.containsKey("rtcp-mux")) {

View file

@ -0,0 +1,5 @@
<vector android:height="24dp" android:tint="#FFFFFF"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="@android:color/white" android:pathData="M9,16.17L4.83,12l-1.42,1.41L9,19 21,7l-1.41,-1.41z"/>
</vector>

View file

@ -13,4 +13,7 @@
android:icon="?attr/icon_goto_chat"
android:title="@string/switch_to_conversation"
app:showAsAction="always" />
<item android:id="@+id/action_switch_to_video"
android:title="@string/switch_to_video"
app:showAsAction="never"/>
</menu>

View file

@ -1068,6 +1068,8 @@
<string name="rtp_state_declined_or_busy">Busy</string>
<string name="rtp_state_connectivity_error">Unable to connect call</string>
<string name="rtp_state_connectivity_lost_error">Connection lost</string>
<string name="rtp_state_content_add_video">Switch to video call?</string>
<string name="rtp_state_content_add">Add additional tracks?</string>
<string name="rtp_state_retracted">Retracted call</string>
<string name="rtp_state_application_failure">Application failure</string>
<string name="rtp_state_security_error">Verification problem</string>
@ -1241,4 +1243,6 @@
<string name="audio_video_disabled_tor">Calls are disabled when using Tor</string>
<string name="pref_enable_persistent_rooms_summary">Make group chats and public channels persistent and do not delete them from the server after the last user left the chat.</string>
<string name="pref_enable_persistent_rooms_title">Persistent group chats</string>
<string name="switch_to_video">Switch to video</string>
<string name="reject_switch_to_video">Reject switch to video request</string>
</resources>