prepare JingleRtpConnection for content-adds
This commit is contained in:
parent
63501adc45
commit
4e8ceadfbf
|
@ -33,6 +33,7 @@ import java.util.concurrent.CountDownLatch;
|
||||||
|
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.utils.AppRTCUtils;
|
import eu.siacs.conversations.utils.AppRTCUtils;
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.Media;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
|
* AppRTCAudioManager manages all audio related parts of the AppRTC demo.
|
||||||
|
@ -44,7 +45,7 @@ public class AppRTCAudioManager {
|
||||||
private final Context apprtcContext;
|
private final Context apprtcContext;
|
||||||
// Contains speakerphone setting: auto, true or false
|
// Contains speakerphone setting: auto, true or false
|
||||||
@Nullable
|
@Nullable
|
||||||
private final SpeakerPhonePreference speakerPhonePreference;
|
private SpeakerPhonePreference speakerPhonePreference;
|
||||||
// Handles all tasks related to Bluetooth headset devices.
|
// Handles all tasks related to Bluetooth headset devices.
|
||||||
private final AppRTCBluetoothManager bluetoothManager;
|
private final AppRTCBluetoothManager bluetoothManager;
|
||||||
@Nullable
|
@Nullable
|
||||||
|
@ -110,6 +111,16 @@ public class AppRTCAudioManager {
|
||||||
AppRTCUtils.logDeviceInfo(Config.LOGTAG);
|
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.
|
* Construction.
|
||||||
*/
|
*/
|
||||||
|
@ -587,7 +598,15 @@ public class AppRTCAudioManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SpeakerPhonePreference {
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,88 @@
|
||||||
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import com.google.common.base.MoreObjects;
|
||||||
|
import com.google.common.base.Objects;
|
||||||
|
import com.google.common.collect.Collections2;
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
|
||||||
|
|
||||||
|
public final class ContentAddition {
|
||||||
|
|
||||||
|
public final Direction direction;
|
||||||
|
public final Set<Summary> summary;
|
||||||
|
|
||||||
|
private ContentAddition(Direction direction, Set<Summary> summary) {
|
||||||
|
this.direction = direction;
|
||||||
|
this.summary = summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<Media> media() {
|
||||||
|
return ImmutableSet.copyOf(Collections2.transform(summary, s -> s.media));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static ContentAddition of(final Direction direction, final RtpContentMap rtpContentMap) {
|
||||||
|
return new ContentAddition(direction, summary(rtpContentMap));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Set<Summary> summary(final RtpContentMap rtpContentMap) {
|
||||||
|
return ImmutableSet.copyOf(
|
||||||
|
Collections2.transform(
|
||||||
|
rtpContentMap.contents.entrySet(),
|
||||||
|
e -> {
|
||||||
|
final RtpContentMap.DescriptionTransport dt = e.getValue();
|
||||||
|
return new Summary(e.getKey(), dt.description.getMedia(), dt.senders);
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this)
|
||||||
|
.add("direction", direction)
|
||||||
|
.add("summary", summary)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum Direction {
|
||||||
|
OUTGOING,
|
||||||
|
INCOMING
|
||||||
|
}
|
||||||
|
|
||||||
|
public static final class Summary {
|
||||||
|
public final String name;
|
||||||
|
public final Media media;
|
||||||
|
public final Content.Senders senders;
|
||||||
|
|
||||||
|
private Summary(final String name, final Media media, final Content.Senders senders) {
|
||||||
|
this.name = name;
|
||||||
|
this.media = media;
|
||||||
|
this.senders = senders;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) return true;
|
||||||
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
|
Summary summary = (Summary) o;
|
||||||
|
return Objects.equal(name, summary.name)
|
||||||
|
&& media == summary.media
|
||||||
|
&& senders == summary.senders;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hashCode(name, media, senders);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return MoreObjects.toStringHelper(this)
|
||||||
|
.add("name", name)
|
||||||
|
.add("media", media)
|
||||||
|
.add("senders", senders)
|
||||||
|
.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,16 +5,16 @@ import android.util.Log;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.google.common.base.Joiner;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
import com.google.common.base.Predicates;
|
|
||||||
import com.google.common.base.Stopwatch;
|
import com.google.common.base.Stopwatch;
|
||||||
import com.google.common.base.Strings;
|
import com.google.common.base.Strings;
|
||||||
import com.google.common.base.Throwables;
|
import com.google.common.base.Throwables;
|
||||||
import com.google.common.collect.Collections2;
|
import com.google.common.collect.Collections2;
|
||||||
import com.google.common.collect.ImmutableList;
|
import com.google.common.collect.ImmutableList;
|
||||||
import com.google.common.collect.ImmutableMap;
|
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.collect.Sets;
|
||||||
import com.google.common.primitives.Ints;
|
import com.google.common.primitives.Ints;
|
||||||
import com.google.common.util.concurrent.FutureCallback;
|
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.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import eu.siacs.conversations.BuildConfig;
|
||||||
import eu.siacs.conversations.Config;
|
import eu.siacs.conversations.Config;
|
||||||
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
import eu.siacs.conversations.crypto.axolotl.AxolotlService;
|
||||||
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
|
import eu.siacs.conversations.crypto.axolotl.CryptoFailedException;
|
||||||
|
@ -53,6 +54,7 @@ import eu.siacs.conversations.utils.IP;
|
||||||
import eu.siacs.conversations.xml.Element;
|
import eu.siacs.conversations.xml.Element;
|
||||||
import eu.siacs.conversations.xml.Namespace;
|
import eu.siacs.conversations.xml.Namespace;
|
||||||
import eu.siacs.conversations.xmpp.Jid;
|
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.Group;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.IceUdpTransportInfo;
|
||||||
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
import eu.siacs.conversations.xmpp.jingle.stanzas.JinglePacket;
|
||||||
|
@ -163,6 +165,8 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
private Set<Media> proposedMedia;
|
private Set<Media> proposedMedia;
|
||||||
private RtpContentMap initiatorRtpContentMap;
|
private RtpContentMap initiatorRtpContentMap;
|
||||||
private RtpContentMap responderRtpContentMap;
|
private RtpContentMap responderRtpContentMap;
|
||||||
|
private RtpContentMap incomingContentAdd;
|
||||||
|
private RtpContentMap outgoingContentAdd;
|
||||||
private IceUdpTransportInfo.Setup peerDtlsSetup;
|
private IceUdpTransportInfo.Setup peerDtlsSetup;
|
||||||
private final Stopwatch sessionDuration = Stopwatch.createUnstarted();
|
private final Stopwatch sessionDuration = Stopwatch.createUnstarted();
|
||||||
private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
|
private final Queue<PeerConnection.PeerConnectionState> stateHistory = new LinkedList<>();
|
||||||
|
@ -218,6 +222,18 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
case SESSION_TERMINATE:
|
case SESSION_TERMINATE:
|
||||||
receiveSessionTerminate(jinglePacket);
|
receiveSessionTerminate(jinglePacket);
|
||||||
break;
|
break;
|
||||||
|
case CONTENT_ADD:
|
||||||
|
receiveContentAdd(jinglePacket);
|
||||||
|
break;
|
||||||
|
case CONTENT_ACCEPT:
|
||||||
|
receiveContentAccept(jinglePacket);
|
||||||
|
break;
|
||||||
|
case CONTENT_REJECT:
|
||||||
|
receiveContentReject(jinglePacket);
|
||||||
|
break;
|
||||||
|
case CONTENT_REMOVE:
|
||||||
|
receiveContentRemove(jinglePacket);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
respondOk(jinglePacket);
|
respondOk(jinglePacket);
|
||||||
Log.d(
|
Log.d(
|
||||||
|
@ -346,6 +362,405 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
id.getAccount().getJid().asBareJid() + ": improperly formatted contents",
|
||||||
|
Throwables.getRootCause(e));
|
||||||
|
respondOk(jinglePacket);
|
||||||
|
webRTCWrapper.close();
|
||||||
|
sendSessionTerminate(Reason.of(e), e.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final RtpContentMap outgoingContentAdd = this.outgoingContentAdd;
|
||||||
|
if (outgoingContentAdd == null) {
|
||||||
|
Log.d(Config.LOGTAG, "received content-accept when we had no outgoing content add");
|
||||||
|
terminateWithOutOfOrder(jinglePacket);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Set<ContentAddition.Summary> ourSummary = ContentAddition.summary(outgoingContentAdd);
|
||||||
|
if (ourSummary.equals(ContentAddition.summary(receivedContentAccept))) {
|
||||||
|
this.outgoingContentAdd = null;
|
||||||
|
respondOk(jinglePacket);
|
||||||
|
receiveContentAccept(receivedContentAccept);
|
||||||
|
} else {
|
||||||
|
Log.d(Config.LOGTAG, "received content-accept did not match our outgoing content-add");
|
||||||
|
terminateWithOutOfOrder(jinglePacket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receiveContentAccept(final RtpContentMap receivedContentAccept) {
|
||||||
|
final IceUdpTransportInfo.Setup peerDtlsSetup = getPeerDtlsSetup();
|
||||||
|
final RtpContentMap modifiedContentMap =
|
||||||
|
getRemoteContentMap().addContent(receivedContentAccept, peerDtlsSetup);
|
||||||
|
|
||||||
|
setRemoteContentMap(modifiedContentMap);
|
||||||
|
|
||||||
|
final SessionDescription answer = SessionDescription.of(modifiedContentMap, !isInitiator());
|
||||||
|
|
||||||
|
final org.webrtc.SessionDescription sdp =
|
||||||
|
new org.webrtc.SessionDescription(
|
||||||
|
org.webrtc.SessionDescription.Type.ANSWER, answer.toString());
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.webRTCWrapper.setRemoteDescription(sdp).get();
|
||||||
|
} catch (final Exception e) {
|
||||||
|
final Throwable cause = Throwables.getRootCause(e);
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
id.getAccount().getJid().asBareJid()
|
||||||
|
+ ": unable to set remote description after receiving content-accept",
|
||||||
|
cause);
|
||||||
|
webRTCWrapper.close();
|
||||||
|
sendSessionTerminate(Reason.FAILED_APPLICATION, cause.getMessage());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updateEndUserState();
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
id.getAccount().getJid().asBareJid()
|
||||||
|
+ ": remote has accepted content-add "
|
||||||
|
+ ContentAddition.summary(receivedContentAccept));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receiveContentReject(final JinglePacket jinglePacket) {
|
||||||
|
final RtpContentMap receivedContentReject;
|
||||||
|
try {
|
||||||
|
receivedContentReject = RtpContentMap.of(jinglePacket);
|
||||||
|
} 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
final RtpContentMap outgoingContentAdd = this.outgoingContentAdd;
|
||||||
|
if (outgoingContentAdd == null) {
|
||||||
|
Log.d(Config.LOGTAG, "received content-reject when we had no outgoing content add");
|
||||||
|
terminateWithOutOfOrder(jinglePacket);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final Set<ContentAddition.Summary> ourSummary = ContentAddition.summary(outgoingContentAdd);
|
||||||
|
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");
|
||||||
|
terminateWithOutOfOrder(jinglePacket);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void receiveContentReject(final Set<ContentAddition.Summary> summary) {
|
||||||
|
try {
|
||||||
|
this.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 receiving content-reject",
|
||||||
|
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.APP_NAME,
|
||||||
|
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) {
|
||||||
|
final RtpContentMap incomingContentAdd = this.incomingContentAdd;
|
||||||
|
if (incomingContentAdd == null) {
|
||||||
|
throw new IllegalStateException("No incoming content add");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentAddition.equals(ContentAddition.summary(incomingContentAdd))) {
|
||||||
|
this.incomingContentAdd = null;
|
||||||
|
acceptContentAdd(contentAddition, incomingContentAdd);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("Accepted content add does not match pending content-add");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void acceptContentAdd(@NonNull final Set<ContentAddition.Summary> contentAddition, final RtpContentMap incomingContentAdd) {
|
||||||
|
final IceUdpTransportInfo.Setup setup = getPeerDtlsSetup();
|
||||||
|
final RtpContentMap modifiedContentMap = getRemoteContentMap().addContent(incomingContentAdd, setup);
|
||||||
|
this.setRemoteContentMap(modifiedContentMap);
|
||||||
|
|
||||||
|
final SessionDescription offer;
|
||||||
|
try {
|
||||||
|
offer = SessionDescription.of(modifiedContentMap, !isInitiator());
|
||||||
|
} catch (final IllegalArgumentException | NullPointerException 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;
|
||||||
|
}
|
||||||
|
this.incomingContentAdd = null;
|
||||||
|
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());
|
||||||
|
try {
|
||||||
|
this.webRTCWrapper.setRemoteDescription(sdp).get();
|
||||||
|
|
||||||
|
// TODO add tracks for 'media' where contentAddition.senders matches
|
||||||
|
|
||||||
|
// TODO if senders.sending(isInitiator())
|
||||||
|
|
||||||
|
this.webRTCWrapper.addTrack(Media.VIDEO);
|
||||||
|
|
||||||
|
// TODO add additional transceivers for recv only cases
|
||||||
|
|
||||||
|
final SessionDescription answer = setLocalSessionDescription();
|
||||||
|
final RtpContentMap rtpContentMap = RtpContentMap.of(answer, isInitiator());
|
||||||
|
|
||||||
|
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));
|
||||||
|
webRTCWrapper.close();
|
||||||
|
sendSessionTerminate(Reason.FAILED_APPLICATION);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void sendContentAccept(final RtpContentMap contentAcceptMap) {
|
||||||
|
final JinglePacket jinglePacket = contentAcceptMap.toJinglePacket(JinglePacket.Action.CONTENT_ACCEPT, id.sessionId);
|
||||||
|
send(jinglePacket);
|
||||||
|
}
|
||||||
|
|
||||||
|
public synchronized void rejectContentAdd() {
|
||||||
|
final RtpContentMap incomingContentAdd = this.incomingContentAdd;
|
||||||
|
if (incomingContentAdd == null) {
|
||||||
|
throw new IllegalStateException("No incoming content add");
|
||||||
|
}
|
||||||
|
this.incomingContentAdd = null;
|
||||||
|
updateEndUserState();
|
||||||
|
rejectContentAdd(incomingContentAdd);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void rejectContentAdd(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 boolean checkForIceRestart(
|
private boolean checkForIceRestart(
|
||||||
final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) {
|
final JinglePacket jinglePacket, final RtpContentMap rtpContentMap) {
|
||||||
final RtpContentMap existing = getRemoteContentMap();
|
final RtpContentMap existing = getRemoteContentMap();
|
||||||
|
@ -1534,6 +1949,10 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
return RtpEndUserState.CONNECTING;
|
return RtpEndUserState.CONNECTING;
|
||||||
}
|
}
|
||||||
case SESSION_ACCEPTED:
|
case SESSION_ACCEPTED:
|
||||||
|
final ContentAddition ca = getPendingContentAddition();
|
||||||
|
if (ca != null && ca.direction == ContentAddition.Direction.INCOMING) {
|
||||||
|
return RtpEndUserState.INCOMING_CONTENT_ADD;
|
||||||
|
}
|
||||||
return getPeerConnectionStateAsEndUserState();
|
return getPeerConnectionStateAsEndUserState();
|
||||||
case REJECTED:
|
case REJECTED:
|
||||||
case REJECTED_RACED:
|
case REJECTED_RACED:
|
||||||
|
@ -1591,6 +2010,18 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ContentAddition getPendingContentAddition() {
|
||||||
|
final RtpContentMap in = this.incomingContentAdd;
|
||||||
|
final RtpContentMap out = this.outgoingContentAdd;
|
||||||
|
if (out != null) {
|
||||||
|
return ContentAddition.of(ContentAddition.Direction.OUTGOING, out);
|
||||||
|
} else if (in != null) {
|
||||||
|
return ContentAddition.of(ContentAddition.Direction.INCOMING, in);
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public Set<Media> getMedia() {
|
public Set<Media> getMedia() {
|
||||||
final State current = getState();
|
final State current = getState();
|
||||||
if (current == State.NULL) {
|
if (current == State.NULL) {
|
||||||
|
@ -1604,14 +2035,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
return Preconditions.checkNotNull(
|
return Preconditions.checkNotNull(
|
||||||
this.proposedMedia, "RTP connection has not been initialized properly");
|
this.proposedMedia, "RTP connection has not been initialized properly");
|
||||||
}
|
}
|
||||||
|
final RtpContentMap localContentMap = getLocalContentMap();
|
||||||
final RtpContentMap initiatorContentMap = initiatorRtpContentMap;
|
final RtpContentMap initiatorContentMap = initiatorRtpContentMap;
|
||||||
if (initiatorContentMap != null) {
|
if (localContentMap != null) {
|
||||||
|
return localContentMap.getMedia();
|
||||||
|
} else if (initiatorContentMap != null) {
|
||||||
return initiatorContentMap.getMedia();
|
return initiatorContentMap.getMedia();
|
||||||
} else if (isTerminated()) {
|
} 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 {
|
} else {
|
||||||
return Preconditions.checkNotNull(
|
return Preconditions.checkNotNull(this.proposedMedia, "RTP connection has not been initialized properly");
|
||||||
this.proposedMedia, "RTP connection has not been initialized properly");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1625,6 +2058,16 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
return status != null && status.isVerified();
|
return status != null && status.isVerified();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean addMedia(final Media media) {
|
||||||
|
final Set<Media> currentMedia = getMedia();
|
||||||
|
if (currentMedia.contains(media)) {
|
||||||
|
throw new IllegalStateException(String.format("%s has already been proposed", media));
|
||||||
|
}
|
||||||
|
// TODO add state protection - can only add while ACCEPTED or so
|
||||||
|
Log.d(Config.LOGTAG,"adding media: "+media);
|
||||||
|
return webRTCWrapper.addTrack(media);
|
||||||
|
}
|
||||||
|
|
||||||
public synchronized void acceptCall() {
|
public synchronized void acceptCall() {
|
||||||
switch (this.state) {
|
switch (this.state) {
|
||||||
case PROPOSED:
|
case PROPOSED:
|
||||||
|
@ -1743,17 +2186,9 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
finish();
|
finish();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupWebRTC(
|
private void setupWebRTC(final Set<Media> media, final List<PeerConnection.IceServer> iceServers) throws WebRTCWrapper.InitializationException {
|
||||||
final Set<Media> media, final List<PeerConnection.IceServer> iceServers)
|
|
||||||
throws WebRTCWrapper.InitializationException {
|
|
||||||
this.jingleConnectionManager.ensureConnectionIsRegistered(this);
|
this.jingleConnectionManager.ensureConnectionIsRegistered(this);
|
||||||
final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference;
|
this.webRTCWrapper.setup(this.xmppConnectionService, AppRTCAudioManager.SpeakerPhonePreference.of(media));
|
||||||
if (media.contains(Media.VIDEO)) {
|
|
||||||
speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.SPEAKER;
|
|
||||||
} else {
|
|
||||||
speakerPhonePreference = AppRTCAudioManager.SpeakerPhonePreference.EARPIECE;
|
|
||||||
}
|
|
||||||
this.webRTCWrapper.setup(this.xmppConnectionService, speakerPhonePreference);
|
|
||||||
this.webRTCWrapper.initializePeerConnection(media, iceServers);
|
this.webRTCWrapper.initializePeerConnection(media, iceServers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1905,21 +2340,23 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection);
|
webRTCWrapper.execute(this::closeWebRTCSessionAfterFailedConnection);
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
webRTCWrapper.restartIce();
|
this.restartIce();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateEndUserState();
|
updateEndUserState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void restartIce() {
|
||||||
|
this.stateHistory.clear();
|
||||||
|
this.webRTCWrapper.restartIce();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onRenegotiationNeeded() {
|
public void onRenegotiationNeeded() {
|
||||||
this.webRTCWrapper.execute(this::renegotiate);
|
this.webRTCWrapper.execute(this::renegotiate);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renegotiate() {
|
private void 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;
|
final SessionDescription sessionDescription;
|
||||||
try {
|
try {
|
||||||
sessionDescription = setLocalSessionDescription();
|
sessionDescription = setLocalSessionDescription();
|
||||||
|
@ -1945,19 +2382,26 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
|
|
||||||
if (diff.hasModifications() && iceRestart) {
|
if (diff.hasModifications() && iceRestart) {
|
||||||
webRTCWrapper.close();
|
webRTCWrapper.close();
|
||||||
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (iceRestart) {
|
if (iceRestart) {
|
||||||
initiateIceRestart(rtpContentMap);
|
initiateIceRestart(rtpContentMap);
|
||||||
return;
|
return;
|
||||||
|
} else if (diff.isEmpty()) {
|
||||||
|
Log.d(
|
||||||
|
Config.LOGTAG,
|
||||||
|
"renegotiation. nothing to do. SignalingState="
|
||||||
|
+ this.webRTCWrapper.getSignalingState());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (diff.added.size() > 0) {
|
if (diff.added.size() > 0) {
|
||||||
sendContentAdd(rtpContentMap);
|
modifyLocalContentMap(rtpContentMap);
|
||||||
|
sendContentAdd(rtpContentMap, diff.added);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initiateIceRestart(final RtpContentMap rtpContentMap) {
|
private void initiateIceRestart(final RtpContentMap rtpContentMap) {
|
||||||
|
@ -1977,8 +2421,7 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (response.getType() == IqPacket.TYPE.ERROR) {
|
if (response.getType() == IqPacket.TYPE.ERROR) {
|
||||||
final Element error = response.findChild("error");
|
if (isTieBreak(response)) {
|
||||||
if (error != null && error.hasChild("tie-break", Namespace.JINGLE_ERRORS)) {
|
|
||||||
Log.d(Config.LOGTAG, "received tie-break as result of ice restart");
|
Log.d(Config.LOGTAG, "received tie-break as result of ice restart");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1990,8 +2433,40 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void sendContentAdd(final RtpContentMap rtpContentMap) {
|
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);
|
||||||
|
this.outgoingContentAdd = contentAdd;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setLocalContentMap(final RtpContentMap rtpContentMap) {
|
private void setLocalContentMap(final RtpContentMap rtpContentMap) {
|
||||||
|
@ -2010,6 +2485,15 @@ public class JingleRtpConnection extends AbstractJingleConnection
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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()
|
private SessionDescription setLocalSessionDescription()
|
||||||
throws ExecutionException, InterruptedException {
|
throws ExecutionException, InterruptedException {
|
||||||
final org.webrtc.SessionDescription sessionDescription =
|
final org.webrtc.SessionDescription sessionDescription =
|
||||||
|
|
|
@ -1,11 +1,18 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import com.google.common.collect.ImmutableSet;
|
||||||
|
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import javax.annotation.Nonnull;
|
||||||
|
|
||||||
public enum Media {
|
public enum Media {
|
||||||
|
|
||||||
VIDEO, AUDIO, UNKNOWN;
|
VIDEO, AUDIO, UNKNOWN;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@Nonnull
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return super.toString().toLowerCase(Locale.ROOT);
|
return super.toString().toLowerCase(Locale.ROOT);
|
||||||
}
|
}
|
||||||
|
@ -17,4 +24,12 @@ public enum Media {
|
||||||
return UNKNOWN;
|
return UNKNOWN;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static boolean audioOnly(Set<Media> media) {
|
||||||
|
return ImmutableSet.of(AUDIO).equals(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean videoOnly(Set<Media> media) {
|
||||||
|
return ImmutableSet.of(VIDEO).equals(media);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ import com.google.common.collect.Maps;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
@ -92,6 +93,10 @@ public class RtpContentMap {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Set<Content.Senders> getSenders() {
|
||||||
|
return ImmutableSet.copyOf(Collections2.transform(contents.values(),dt -> dt.senders));
|
||||||
|
}
|
||||||
|
|
||||||
public List<String> getNames() {
|
public List<String> getNames() {
|
||||||
return ImmutableList.copyOf(contents.keySet());
|
return ImmutableList.copyOf(contents.keySet());
|
||||||
}
|
}
|
||||||
|
@ -281,6 +286,14 @@ public class RtpContentMap {
|
||||||
return new RtpContentMap(this.group, contentMapBuilder.build());
|
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) {
|
public RtpContentMap toContentModification(final Collection<String> modifications) {
|
||||||
return new RtpContentMap(
|
return new RtpContentMap(
|
||||||
this.group,
|
this.group,
|
||||||
|
@ -291,6 +304,22 @@ public class RtpContentMap {
|
||||||
dt.senders, dt.description, IceUdpTransportInfo.STUB)));
|
dt.senders, dt.description, IceUdpTransportInfo.STUB)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public RtpContentMap toStub() {
|
||||||
|
return new RtpContentMap(
|
||||||
|
null,
|
||||||
|
Maps.transformValues(
|
||||||
|
this.contents,
|
||||||
|
dt ->
|
||||||
|
new DescriptionTransport(
|
||||||
|
dt.senders,
|
||||||
|
RtpDescription.stub(dt.description.getMedia()),
|
||||||
|
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) {
|
public Diff diff(final RtpContentMap rtpContentMap) {
|
||||||
final Set<String> existingContentIds = this.contents.keySet();
|
final Set<String> existingContentIds = this.contents.keySet();
|
||||||
final Set<String> newContentIds = rtpContentMap.contents.keySet();
|
final Set<String> newContentIds = rtpContentMap.contents.keySet();
|
||||||
|
@ -307,24 +336,32 @@ public class RtpContentMap {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public RtpContentMap addContent(final RtpContentMap modification) {
|
public RtpContentMap addContent(
|
||||||
|
final RtpContentMap modification, final IceUdpTransportInfo.Setup setup) {
|
||||||
final IceUdpTransportInfo.Credentials credentials = getDistinctCredentials();
|
final IceUdpTransportInfo.Credentials credentials = getDistinctCredentials();
|
||||||
final DTLS dtls = getDistinctDtls();
|
final DTLS dtls = getDistinctDtls();
|
||||||
final IceUdpTransportInfo iceUdpTransportInfo =
|
final IceUdpTransportInfo iceUdpTransportInfo =
|
||||||
IceUdpTransportInfo.of(credentials, dtls.setup, dtls.hash, dtls.fingerprint);
|
IceUdpTransportInfo.of(credentials, setup, dtls.hash, dtls.fingerprint);
|
||||||
final Map<String, DescriptionTransport> combined =
|
final Map<String, DescriptionTransport> combined = merge(contents, modification.contents);
|
||||||
new ImmutableMap.Builder<String, DescriptionTransport>()
|
/*new ImmutableMap.Builder<String, DescriptionTransport>()
|
||||||
.putAll(contents)
|
.putAll(contents)
|
||||||
.putAll(
|
.putAll(modification.contents)
|
||||||
|
.build();*/
|
||||||
|
final Map<String, DescriptionTransport> combinedFixedTransport =
|
||||||
Maps.transformValues(
|
Maps.transformValues(
|
||||||
modification.contents,
|
combined,
|
||||||
dt ->
|
dt ->
|
||||||
new DescriptionTransport(
|
new DescriptionTransport(
|
||||||
dt.senders,
|
dt.senders, dt.description, iceUdpTransportInfo));
|
||||||
dt.description,
|
return new RtpContentMap(modification.group, combinedFixedTransport);
|
||||||
iceUdpTransportInfo)))
|
}
|
||||||
.build();
|
|
||||||
return new RtpContentMap(modification.group, combined);
|
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 static class DescriptionTransport {
|
||||||
|
@ -410,6 +447,10 @@ public class RtpContentMap {
|
||||||
return !this.added.isEmpty() || !this.removed.isEmpty();
|
return !this.added.isEmpty() || !this.removed.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return this.added.isEmpty() && this.removed.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Nonnull
|
@Nonnull
|
||||||
public String toString() {
|
public String toString() {
|
||||||
|
|
|
@ -5,6 +5,7 @@ public enum RtpEndUserState {
|
||||||
CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet
|
CONNECTING, //session-initiate or session-accepted but no webrtc peer connection yet
|
||||||
CONNECTED, //session-accepted and webrtc peer connection is connected
|
CONNECTED, //session-accepted and webrtc peer connection is connected
|
||||||
RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed
|
RECONNECTING, //session-accepted and webrtc peer connection was connected once but is currently disconnected or failed
|
||||||
|
INCOMING_CONTENT_ADD, //session-accepted with a pending, incoming content-add
|
||||||
FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
|
FINDING_DEVICE, //'propose' has been sent out; no 184 ack yet
|
||||||
RINGING, //'propose' has been sent out and it has been 184 acked
|
RINGING, //'propose' has been sent out and it has been 184 acked
|
||||||
ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received
|
ACCEPTING_CALL, //'proceed' message has been sent; but no session-initiate has been received
|
||||||
|
|
|
@ -298,7 +298,7 @@ public class SessionDescription {
|
||||||
mediaAttributes.put("mid", name);
|
mediaAttributes.put("mid", name);
|
||||||
|
|
||||||
mediaAttributes.put(descriptionTransport.senders.asMediaAttribute(isInitiatorContentMap), "");
|
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", "");
|
mediaAttributes.put("rtcp-mux", "");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,7 @@ import android.media.AudioManager;
|
||||||
import android.media.ToneGenerator;
|
import android.media.ToneGenerator;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.concurrent.ScheduledFuture;
|
import java.util.concurrent.ScheduledFuture;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
@ -19,6 +20,7 @@ class ToneManager {
|
||||||
private final Context context;
|
private final Context context;
|
||||||
|
|
||||||
private ToneState state = null;
|
private ToneState state = null;
|
||||||
|
private RtpEndUserState endUserState = null;
|
||||||
private ScheduledFuture<?> currentTone;
|
private ScheduledFuture<?> currentTone;
|
||||||
private ScheduledFuture<?> currentResetFuture;
|
private ScheduledFuture<?> currentResetFuture;
|
||||||
private boolean appRtcAudioManagerHasControl = false;
|
private boolean appRtcAudioManagerHasControl = false;
|
||||||
|
@ -51,7 +53,11 @@ class ToneManager {
|
||||||
return ToneState.ENDING_CALL;
|
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)) {
|
if (media.contains(Media.VIDEO)) {
|
||||||
return ToneState.NULL;
|
return ToneState.NULL;
|
||||||
} else {
|
} else {
|
||||||
|
@ -62,14 +68,19 @@ class ToneManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
void transition(final RtpEndUserState state, final Set<Media> media) {
|
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) {
|
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) {
|
if (this.state == state) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -105,6 +116,18 @@ class ToneManager {
|
||||||
this.state = state;
|
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) {
|
void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) {
|
||||||
this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl;
|
this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
package eu.siacs.conversations.xmpp.jingle;
|
package eu.siacs.conversations.xmpp.jingle;
|
||||||
|
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
|
import com.google.common.base.CaseFormat;
|
||||||
import com.google.common.base.Optional;
|
import com.google.common.base.Optional;
|
||||||
import com.google.common.base.Preconditions;
|
import com.google.common.base.Preconditions;
|
||||||
|
|
||||||
import org.webrtc.MediaStreamTrack;
|
import org.webrtc.MediaStreamTrack;
|
||||||
import org.webrtc.PeerConnection;
|
import org.webrtc.PeerConnection;
|
||||||
import org.webrtc.RtpSender;
|
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> {
|
class TrackWrapper<T extends MediaStreamTrack> {
|
||||||
private final T track;
|
public final T track;
|
||||||
private final RtpSender rtpSender;
|
public final RtpSender rtpSender;
|
||||||
|
|
||||||
private TrackWrapper(final T track, final RtpSender rtpSender) {
|
private TrackWrapper(final T track, final RtpSender rtpSender) {
|
||||||
Preconditions.checkNotNull(track);
|
Preconditions.checkNotNull(track);
|
||||||
|
@ -25,7 +36,41 @@ class TrackWrapper<T extends MediaStreamTrack> {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static <T extends MediaStreamTrack> Optional<T> get(
|
public static <T extends MediaStreamTrack> Optional<T> get(
|
||||||
final TrackWrapper<T> trackWrapper) {
|
@Nullable final PeerConnection peerConnection, final TrackWrapper<T> trackWrapper) {
|
||||||
return trackWrapper == null ? Optional.absent() : Optional.of(trackWrapper.track);
|
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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -225,7 +225,7 @@ public class WebRTCWrapper {
|
||||||
|
|
||||||
public void setup(
|
public void setup(
|
||||||
final XmppConnectionService service,
|
final XmppConnectionService service,
|
||||||
final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
|
@Nonnull final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference)
|
||||||
throws InitializationException {
|
throws InitializationException {
|
||||||
try {
|
try {
|
||||||
PeerConnectionFactory.initialize(
|
PeerConnectionFactory.initialize(
|
||||||
|
@ -330,18 +330,35 @@ public class WebRTCWrapper {
|
||||||
throw new IllegalStateException(String.format("Could not add track for %s", media));
|
throw new IllegalStateException(String.format("Could not add track for %s", media));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public synchronized void removeTrack(final Media media) {
|
||||||
|
if (media == Media.VIDEO) {
|
||||||
|
removeVideoTrack(requirePeerConnection());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean addAudioTrack(final PeerConnection peerConnection) {
|
private boolean addAudioTrack(final PeerConnection peerConnection) {
|
||||||
final AudioSource audioSource =
|
final AudioSource audioSource =
|
||||||
requirePeerConnectionFactory().createAudioSource(new MediaConstraints());
|
requirePeerConnectionFactory().createAudioSource(new MediaConstraints());
|
||||||
final AudioTrack audioTrack =
|
final AudioTrack audioTrack =
|
||||||
requirePeerConnectionFactory().createAudioTrack("my-audio-track", audioSource);
|
requirePeerConnectionFactory()
|
||||||
|
.createAudioTrack(TrackWrapper.id(AudioTrack.class), audioSource);
|
||||||
this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack);
|
this.localAudioTrack = TrackWrapper.addTrack(peerConnection, audioTrack);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean addVideoTrack(final PeerConnection peerConnection) {
|
private boolean addVideoTrack(final PeerConnection peerConnection) {
|
||||||
Preconditions.checkState(
|
final TrackWrapper<VideoTrack> existing = this.localVideoTrack;
|
||||||
this.localVideoTrack == null, "A local video track already exists");
|
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;
|
final VideoSourceWrapper videoSourceWrapper;
|
||||||
try {
|
try {
|
||||||
videoSourceWrapper = initializeVideoSourceWrapper();
|
videoSourceWrapper = initializeVideoSourceWrapper();
|
||||||
|
@ -351,11 +368,34 @@ public class WebRTCWrapper {
|
||||||
}
|
}
|
||||||
final VideoTrack videoTrack =
|
final VideoTrack videoTrack =
|
||||||
requirePeerConnectionFactory()
|
requirePeerConnectionFactory()
|
||||||
.createVideoTrack("my-video-track", videoSourceWrapper.getVideoSource());
|
.createVideoTrack(
|
||||||
|
TrackWrapper.id(VideoTrack.class),
|
||||||
|
videoSourceWrapper.getVideoSource());
|
||||||
this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack);
|
this.localVideoTrack = TrackWrapper.addTrack(peerConnection, videoTrack);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void removeVideoTrack(final PeerConnection peerConnection) {
|
||||||
|
final TrackWrapper<VideoTrack> localVideoTrack = this.localVideoTrack;
|
||||||
|
if (localVideoTrack != null) {
|
||||||
|
|
||||||
|
final RtpTransceiver exactTransceiver =
|
||||||
|
TrackWrapper.getTransceiver(peerConnection, localVideoTrack);
|
||||||
|
if (exactTransceiver == null) {
|
||||||
|
throw new IllegalStateException();
|
||||||
|
}
|
||||||
|
exactTransceiver.setDirection(RtpTransceiver.RtpTransceiverDirection.INACTIVE);
|
||||||
|
}
|
||||||
|
final VideoSourceWrapper videoSourceWrapper = this.videoSourceWrapper;
|
||||||
|
if (videoSourceWrapper != null) {
|
||||||
|
try {
|
||||||
|
videoSourceWrapper.stopCapture();
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static PeerConnection.RTCConfiguration buildConfiguration(
|
private static PeerConnection.RTCConfiguration buildConfiguration(
|
||||||
final List<PeerConnection.IceServer> iceServers) {
|
final List<PeerConnection.IceServer> iceServers) {
|
||||||
final PeerConnection.RTCConfiguration rtcConfig =
|
final PeerConnection.RTCConfiguration rtcConfig =
|
||||||
|
@ -375,7 +415,12 @@ public class WebRTCWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
void restartIce() {
|
void restartIce() {
|
||||||
executorService.execute(() -> requirePeerConnection().restartIce());
|
executorService.execute(() -> {
|
||||||
|
final PeerConnection peerConnection = requirePeerConnection();
|
||||||
|
setIsReadyToReceiveIceCandidates(false);
|
||||||
|
peerConnection.restartIce();
|
||||||
|
requirePeerConnection().restartIce();}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setIsReadyToReceiveIceCandidates(final boolean ready) {
|
public void setIsReadyToReceiveIceCandidates(final boolean ready) {
|
||||||
|
@ -450,7 +495,8 @@ public class WebRTCWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isMicrophoneEnabled() {
|
boolean isMicrophoneEnabled() {
|
||||||
final Optional<AudioTrack> audioTrack = TrackWrapper.get(this.localAudioTrack);
|
final Optional<AudioTrack> audioTrack =
|
||||||
|
TrackWrapper.get(peerConnection, this.localAudioTrack);
|
||||||
if (audioTrack.isPresent()) {
|
if (audioTrack.isPresent()) {
|
||||||
try {
|
try {
|
||||||
return audioTrack.get().enabled();
|
return audioTrack.get().enabled();
|
||||||
|
@ -465,7 +511,8 @@ public class WebRTCWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean setMicrophoneEnabled(final boolean enabled) {
|
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()) {
|
if (audioTrack.isPresent()) {
|
||||||
try {
|
try {
|
||||||
audioTrack.get().setEnabled(enabled);
|
audioTrack.get().setEnabled(enabled);
|
||||||
|
@ -481,7 +528,8 @@ public class WebRTCWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isVideoEnabled() {
|
boolean isVideoEnabled() {
|
||||||
final Optional<VideoTrack> videoTrack = TrackWrapper.get(this.localVideoTrack);
|
final Optional<VideoTrack> videoTrack =
|
||||||
|
TrackWrapper.get(peerConnection, this.localVideoTrack);
|
||||||
if (videoTrack.isPresent()) {
|
if (videoTrack.isPresent()) {
|
||||||
return videoTrack.get().enabled();
|
return videoTrack.get().enabled();
|
||||||
}
|
}
|
||||||
|
@ -489,7 +537,8 @@ public class WebRTCWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
void setVideoEnabled(final boolean enabled) {
|
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()) {
|
if (videoTrack.isPresent()) {
|
||||||
videoTrack.get().setEnabled(enabled);
|
videoTrack.get().setEnabled(enabled);
|
||||||
return;
|
return;
|
||||||
|
@ -528,7 +577,7 @@ public class WebRTCWrapper {
|
||||||
MoreExecutors.directExecutor());
|
MoreExecutors.directExecutor());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void logDescription(final SessionDescription sessionDescription) {
|
public static void logDescription(final SessionDescription sessionDescription) {
|
||||||
for (final String line :
|
for (final String line :
|
||||||
sessionDescription.description.split(
|
sessionDescription.description.split(
|
||||||
eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
|
eu.siacs.conversations.xmpp.jingle.SessionDescription.LINE_DIVIDER)) {
|
||||||
|
@ -612,7 +661,7 @@ public class WebRTCWrapper {
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<VideoTrack> getLocalVideoTrack() {
|
Optional<VideoTrack> getLocalVideoTrack() {
|
||||||
return TrackWrapper.get(this.localVideoTrack);
|
return TrackWrapper.get(peerConnection, this.localVideoTrack);
|
||||||
}
|
}
|
||||||
|
|
||||||
Optional<VideoTrack> getRemoteVideoTrack() {
|
Optional<VideoTrack> getRemoteVideoTrack() {
|
||||||
|
@ -635,6 +684,10 @@ public class WebRTCWrapper {
|
||||||
executorService.execute(command);
|
executorService.execute(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void switchSpeakerPhonePreference(AppRTCAudioManager.SpeakerPhonePreference preference) {
|
||||||
|
mainHandler.post(() -> appRTCAudioManager.switchSpeakerPhonePreference(preference));
|
||||||
|
}
|
||||||
|
|
||||||
public interface EventCallback {
|
public interface EventCallback {
|
||||||
void onIceCandidate(IceCandidate iceCandidate);
|
void onIceCandidate(IceCandidate iceCandidate);
|
||||||
|
|
||||||
|
|
|
@ -22,7 +22,6 @@ import eu.siacs.conversations.xmpp.jingle.SessionDescription;
|
||||||
|
|
||||||
public class RtpDescription extends GenericDescription {
|
public class RtpDescription extends GenericDescription {
|
||||||
|
|
||||||
|
|
||||||
private RtpDescription(final String media) {
|
private RtpDescription(final String media) {
|
||||||
super("description", Namespace.JINGLE_APPS_RTP);
|
super("description", Namespace.JINGLE_APPS_RTP);
|
||||||
this.setAttribute("media", media);
|
this.setAttribute("media", media);
|
||||||
|
@ -32,6 +31,10 @@ public class RtpDescription extends GenericDescription {
|
||||||
super("description", Namespace.JINGLE_APPS_RTP);
|
super("description", Namespace.JINGLE_APPS_RTP);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static RtpDescription stub(final Media media) {
|
||||||
|
return new RtpDescription(media.toString());
|
||||||
|
}
|
||||||
|
|
||||||
public Media getMedia() {
|
public Media getMedia() {
|
||||||
return Media.of(this.getAttribute("media"));
|
return Media.of(this.getAttribute("media"));
|
||||||
}
|
}
|
||||||
|
@ -57,7 +60,8 @@ public class RtpDescription extends GenericDescription {
|
||||||
public List<RtpHeaderExtension> getHeaderExtensions() {
|
public List<RtpHeaderExtension> getHeaderExtensions() {
|
||||||
final ImmutableList.Builder<RtpHeaderExtension> builder = new ImmutableList.Builder<>();
|
final ImmutableList.Builder<RtpHeaderExtension> builder = new ImmutableList.Builder<>();
|
||||||
for (final Element child : getChildren()) {
|
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));
|
builder.add(RtpHeaderExtension.upgrade(child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -67,7 +71,9 @@ public class RtpDescription extends GenericDescription {
|
||||||
public List<Source> getSources() {
|
public List<Source> getSources() {
|
||||||
final ImmutableList.Builder<Source> builder = new ImmutableList.Builder<>();
|
final ImmutableList.Builder<Source> builder = new ImmutableList.Builder<>();
|
||||||
for (final Element child : this.children) {
|
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));
|
builder.add(Source.upgrade(child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +83,9 @@ public class RtpDescription extends GenericDescription {
|
||||||
public List<SourceGroup> getSourceGroups() {
|
public List<SourceGroup> getSourceGroups() {
|
||||||
final ImmutableList.Builder<SourceGroup> builder = new ImmutableList.Builder<>();
|
final ImmutableList.Builder<SourceGroup> builder = new ImmutableList.Builder<>();
|
||||||
for (final Element child : this.children) {
|
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));
|
builder.add(SourceGroup.upgrade(child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -85,8 +93,12 @@ public class RtpDescription extends GenericDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static RtpDescription upgrade(final Element element) {
|
public static RtpDescription upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("description".equals(element.getName()), "Name of provided element is not description");
|
Preconditions.checkArgument(
|
||||||
Preconditions.checkArgument(Namespace.JINGLE_APPS_RTP.equals(element.getNamespace()), "Element does not match the jingle rtp namespace");
|
"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();
|
final RtpDescription description = new RtpDescription();
|
||||||
description.setAttributes(element.getAttributes());
|
description.setAttributes(element.getAttributes());
|
||||||
description.setChildren(element.getChildren());
|
description.setChildren(element.getChildren());
|
||||||
|
@ -116,7 +128,8 @@ public class RtpDescription extends GenericDescription {
|
||||||
|
|
||||||
private static FeedbackNegotiation upgrade(final Element element) {
|
private static FeedbackNegotiation upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("rtcp-fb".equals(element.getName()));
|
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();
|
final FeedbackNegotiation feedback = new FeedbackNegotiation();
|
||||||
feedback.setAttributes(element.getAttributes());
|
feedback.setAttributes(element.getAttributes());
|
||||||
feedback.setChildren(element.getChildren());
|
feedback.setChildren(element.getChildren());
|
||||||
|
@ -126,13 +139,13 @@ public class RtpDescription extends GenericDescription {
|
||||||
public static List<FeedbackNegotiation> fromChildren(final List<Element> children) {
|
public static List<FeedbackNegotiation> fromChildren(final List<Element> children) {
|
||||||
ImmutableList.Builder<FeedbackNegotiation> builder = new ImmutableList.Builder<>();
|
ImmutableList.Builder<FeedbackNegotiation> builder = new ImmutableList.Builder<>();
|
||||||
for (final Element child : children) {
|
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));
|
builder.add(upgrade(child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return builder.build();
|
return builder.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class FeedbackNegotiationTrrInt extends Element {
|
public static class FeedbackNegotiationTrrInt extends Element {
|
||||||
|
@ -142,7 +155,6 @@ public class RtpDescription extends GenericDescription {
|
||||||
this.setAttribute("value", value);
|
this.setAttribute("value", value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private FeedbackNegotiationTrrInt() {
|
private FeedbackNegotiationTrrInt() {
|
||||||
super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
|
super("rtcp-fb-trr-int", Namespace.JINGLE_RTP_FEEDBACK_NEGOTIATION);
|
||||||
}
|
}
|
||||||
|
@ -150,12 +162,12 @@ public class RtpDescription extends GenericDescription {
|
||||||
public int getValue() {
|
public int getValue() {
|
||||||
final String value = getAttribute("value");
|
final String value = getAttribute("value");
|
||||||
return Integer.parseInt(value);
|
return Integer.parseInt(value);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static FeedbackNegotiationTrrInt upgrade(final Element element) {
|
private static FeedbackNegotiationTrrInt upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("rtcp-fb-trr-int".equals(element.getName()));
|
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();
|
final FeedbackNegotiationTrrInt trr = new FeedbackNegotiationTrrInt();
|
||||||
trr.setAttributes(element.getAttributes());
|
trr.setAttributes(element.getAttributes());
|
||||||
trr.setChildren(element.getChildren());
|
trr.setChildren(element.getChildren());
|
||||||
|
@ -163,9 +175,11 @@ public class RtpDescription extends GenericDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static List<FeedbackNegotiationTrrInt> fromChildren(final List<Element> children) {
|
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) {
|
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));
|
builder.add(upgrade(child));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -173,9 +187,8 @@ public class RtpDescription extends GenericDescription {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// XEP-0294: Jingle RTP Header Extensions Negotiation
|
||||||
//XEP-0294: Jingle RTP Header Extensions Negotiation
|
// maps to `extmap:$id $uri`
|
||||||
//maps to `extmap:$id $uri`
|
|
||||||
public static class RtpHeaderExtension extends Element {
|
public static class RtpHeaderExtension extends Element {
|
||||||
|
|
||||||
private RtpHeaderExtension() {
|
private RtpHeaderExtension() {
|
||||||
|
@ -198,7 +211,8 @@ public class RtpDescription extends GenericDescription {
|
||||||
|
|
||||||
public static RtpHeaderExtension upgrade(final Element element) {
|
public static RtpHeaderExtension upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("rtp-hdrext".equals(element.getName()));
|
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();
|
final RtpHeaderExtension extension = new RtpHeaderExtension();
|
||||||
extension.setAttributes(element.getAttributes());
|
extension.setAttributes(element.getAttributes());
|
||||||
extension.setChildren(element.getChildren());
|
extension.setChildren(element.getChildren());
|
||||||
|
@ -217,7 +231,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 {
|
public static class PayloadType extends Element {
|
||||||
|
|
||||||
private PayloadType() {
|
private PayloadType() {
|
||||||
|
@ -238,8 +252,14 @@ public class RtpDescription extends GenericDescription {
|
||||||
final int channels = getChannels();
|
final int channels = getChannels();
|
||||||
final String name = getPayloadTypeName();
|
final String name = getPayloadTypeName();
|
||||||
Preconditions.checkArgument(name != null, "Payload-type name must not be empty");
|
Preconditions.checkArgument(name != null, "Payload-type name must not be empty");
|
||||||
SessionDescription.checkNoWhitespace(name, "payload-type name must not contain whitespaces");
|
SessionDescription.checkNoWhitespace(
|
||||||
return getId() + " " + name + "/" + getClockRate() + (channels == 1 ? "" : "/" + channels);
|
name, "payload-type name must not contain whitespaces");
|
||||||
|
return getId()
|
||||||
|
+ " "
|
||||||
|
+ name
|
||||||
|
+ "/"
|
||||||
|
+ getClockRate()
|
||||||
|
+ (channels == 1 ? "" : "/" + channels);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getIntId() {
|
public int getIntId() {
|
||||||
|
@ -251,7 +271,6 @@ public class RtpDescription extends GenericDescription {
|
||||||
return this.getAttribute("id");
|
return this.getAttribute("id");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public String getPayloadTypeName() {
|
public String getPayloadTypeName() {
|
||||||
return this.getAttribute("name");
|
return this.getAttribute("name");
|
||||||
}
|
}
|
||||||
|
@ -271,7 +290,8 @@ public class RtpDescription extends GenericDescription {
|
||||||
public int getChannels() {
|
public int getChannels() {
|
||||||
final String channels = this.getAttribute("channels");
|
final String channels = this.getAttribute("channels");
|
||||||
if (channels == null) {
|
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 {
|
try {
|
||||||
return Integer.parseInt(channels);
|
return Integer.parseInt(channels);
|
||||||
|
@ -299,7 +319,9 @@ public class RtpDescription extends GenericDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static PayloadType of(final Element element) {
|
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 payloadType = new PayloadType();
|
||||||
payloadType.setAttributes(element.getAttributes());
|
payloadType.setAttributes(element.getAttributes());
|
||||||
payloadType.setChildren(element.getChildren());
|
payloadType.setChildren(element.getChildren());
|
||||||
|
@ -339,8 +361,8 @@ public class RtpDescription extends GenericDescription {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//map to `fmtp $id key=value;key=value
|
// map to `fmtp $id key=value;key=value
|
||||||
//where id is the id of the parent payload-type
|
// where id is the id of the parent payload-type
|
||||||
public static class Parameter extends Element {
|
public static class Parameter extends Element {
|
||||||
|
|
||||||
private Parameter() {
|
private Parameter() {
|
||||||
|
@ -362,7 +384,8 @@ public class RtpDescription extends GenericDescription {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Parameter of(final Element element) {
|
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 parameter = new Parameter();
|
||||||
parameter.setAttributes(element.getAttributes());
|
parameter.setAttributes(element.getAttributes());
|
||||||
parameter.setChildren(element.getChildren());
|
parameter.setChildren(element.getChildren());
|
||||||
|
@ -375,12 +398,18 @@ public class RtpDescription extends GenericDescription {
|
||||||
for (int i = 0; i < parameters.size(); ++i) {
|
for (int i = 0; i < parameters.size(); ++i) {
|
||||||
final Parameter p = parameters.get(i);
|
final Parameter p = parameters.get(i);
|
||||||
final String name = p.getParameterName();
|
final String name = p.getParameterName();
|
||||||
Preconditions.checkArgument(name != null, String.format("parameter for %s must have a name", id));
|
Preconditions.checkArgument(
|
||||||
SessionDescription.checkNoWhitespace(name, String.format("parameter names for %s must not contain whitespaces", id));
|
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();
|
final String value = p.getParameterValue();
|
||||||
Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id));
|
Preconditions.checkArgument(
|
||||||
SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id));
|
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);
|
stringBuilder.append(name).append('=').append(value);
|
||||||
if (i != parameters.size() - 1) {
|
if (i != parameters.size() - 1) {
|
||||||
|
@ -393,8 +422,11 @@ public class RtpDescription extends GenericDescription {
|
||||||
public static String toSdpString(final String id, final Parameter parameter) {
|
public static String toSdpString(final String id, final Parameter parameter) {
|
||||||
final String name = parameter.getParameterName();
|
final String name = parameter.getParameterName();
|
||||||
final String value = parameter.getParameterValue();
|
final String value = parameter.getParameterValue();
|
||||||
Preconditions.checkArgument(value != null, String.format("parameter for %s must have a value", id));
|
Preconditions.checkArgument(
|
||||||
SessionDescription.checkNoWhitespace(value, String.format("parameter values for %s must not contain whitespaces", id));
|
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)) {
|
if (Strings.isNullOrEmpty(name)) {
|
||||||
return String.format("%s %s", id, value);
|
return String.format("%s %s", id, value);
|
||||||
} else {
|
} else {
|
||||||
|
@ -420,8 +452,8 @@ public class RtpDescription extends GenericDescription {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//XEP-0339: Source-Specific Media Attributes in Jingle
|
// XEP-0339: Source-Specific Media Attributes in Jingle
|
||||||
//maps to `a=ssrc:<ssrc-id> <attribute>:<value>`
|
// maps to `a=ssrc:<ssrc-id> <attribute>:<value>`
|
||||||
public static class Source extends Element {
|
public static class Source extends Element {
|
||||||
|
|
||||||
private Source() {
|
private Source() {
|
||||||
|
@ -452,7 +484,9 @@ public class RtpDescription extends GenericDescription {
|
||||||
|
|
||||||
public static Source upgrade(final Element element) {
|
public static Source upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("source".equals(element.getName()));
|
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();
|
final Source source = new Source();
|
||||||
source.setChildren(element.getChildren());
|
source.setChildren(element.getChildren());
|
||||||
source.setAttributes(element.getAttributes());
|
source.setAttributes(element.getAttributes());
|
||||||
|
@ -489,7 +523,6 @@ public class RtpDescription extends GenericDescription {
|
||||||
return parameter;
|
return parameter;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static class SourceGroup extends Element {
|
public static class SourceGroup extends Element {
|
||||||
|
@ -525,7 +558,9 @@ public class RtpDescription extends GenericDescription {
|
||||||
|
|
||||||
public static SourceGroup upgrade(final Element element) {
|
public static SourceGroup upgrade(final Element element) {
|
||||||
Preconditions.checkArgument("ssrc-group".equals(element.getName()));
|
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();
|
final SourceGroup group = new SourceGroup();
|
||||||
group.setChildren(element.getChildren());
|
group.setChildren(element.getChildren());
|
||||||
group.setAttributes(element.getAttributes());
|
group.setAttributes(element.getAttributes());
|
||||||
|
@ -533,15 +568,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 RtpDescription rtpDescription = new RtpDescription(media.media);
|
||||||
final Map<String, List<Parameter>> parameterMap = new HashMap<>();
|
final Map<String, List<Parameter>> parameterMap = new HashMap<>();
|
||||||
final ArrayListMultimap<String, Element> feedbackNegotiationMap = ArrayListMultimap.create();
|
final ArrayListMultimap<String, Element> feedbackNegotiationMap =
|
||||||
final ArrayListMultimap<String, Source.Parameter> sourceParameterMap = ArrayListMultimap.create();
|
ArrayListMultimap.create();
|
||||||
final Set<String> attributes = Sets.newHashSet(Iterables.concat(
|
final ArrayListMultimap<String, Source.Parameter> sourceParameterMap =
|
||||||
sessionDescription.attributes.keySet(),
|
ArrayListMultimap.create();
|
||||||
media.attributes.keySet()
|
final Set<String> attributes =
|
||||||
));
|
Sets.newHashSet(
|
||||||
|
Iterables.concat(
|
||||||
|
sessionDescription.attributes.keySet(), media.attributes.keySet()));
|
||||||
for (final String rtcpFb : media.attributes.get("rtcp-fb")) {
|
for (final String rtcpFb : media.attributes.get("rtcp-fb")) {
|
||||||
final String[] parts = rtcpFb.split(" ");
|
final String[] parts = rtcpFb.split(" ");
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
|
@ -550,7 +588,10 @@ public class RtpDescription extends GenericDescription {
|
||||||
final String subType = parts.length >= 3 ? parts[2] : null;
|
final String subType = parts.length >= 3 ? parts[2] : null;
|
||||||
if ("trr-int".equals(type)) {
|
if ("trr-int".equals(type)) {
|
||||||
if (subType != null) {
|
if (subType != null) {
|
||||||
feedbackNegotiationMap.put(id, new FeedbackNegotiationTrrInt(SessionDescription.ignorantIntParser(subType)));
|
feedbackNegotiationMap.put(
|
||||||
|
id,
|
||||||
|
new FeedbackNegotiationTrrInt(
|
||||||
|
SessionDescription.ignorantIntParser(subType)));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType));
|
feedbackNegotiationMap.put(id, new FeedbackNegotiation(type, subType));
|
||||||
|
@ -602,7 +643,8 @@ public class RtpDescription extends GenericDescription {
|
||||||
rtpDescription.addChild(new SourceGroup(semantics, builder.build()));
|
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()));
|
rtpDescription.addChild(new Source(source.getKey(), source.getValue()));
|
||||||
}
|
}
|
||||||
if (media.attributes.containsKey("rtcp-mux")) {
|
if (media.attributes.containsKey("rtcp-mux")) {
|
||||||
|
|
Loading…
Reference in a new issue